Handbooks
संदर्भ

Artifact प्रारूप

pipeline जो भी फ़ाइल लिखती है, उसका schema, और पढ़ते समय उसे कौन सत्यापित करता है।

Toolchain जो भी artifact पढ़ती या लिखती है, pipeline क्रम में। सभी JSON/YAML artifacts में एक version फ़ील्ड होता है और पढ़ते समय उन्हें @handbooks/core की zod schemas से सत्यापित किया जाता है। जब तक अलग से न कहा गया हो, रास्ते विश्लेषित स्रोत मूल के सापेक्ष POSIX रूप में हैं।

Work directory का ढाँचा

<work>/
  phase1/
    graph.json            the call graph (nodes + edges + selfAttrs + metadata)
    functions.csv         one row per internal function
    graph.dot             Graphviz view (files as clusters; await edges colored)
    dropped-calls.json    unresolved calls, categorized
    scan-coverage.json    files the scan could NOT turn into facts, and why
  phase2/
    cards/<rel>.json      one card per source file (tree-mirrored paths)
    cards/_coverage.json  {nFiles, nDescribed, missing[]}
    skeleton.yaml         the stage skeleton
    assignment.json       file → stage
    organization.yaml     intra-stage groups + reading order
    members.json          (member strategy only) function → stage
  phase3/
    narration.json        stage + system prose
    registers.json        cross-stage state registers
    cache/                content-hash caches (safe to delete; costs a re-generation)

phase1/graph.json

{
  "version": 1,
  "metadata": {
    "generatedAt": "2026-08-02T10:00:00.000Z",
    "language": "python | typescript | go | rust | shell | multi",
    "sourceRoot": "/abs/path",
    "scannedFiles": ["aggregate/rollup.rs", "…"], // only files that were actually read and parsed
    "nInternalFunctions": 316,
    "nBoundaryNodes": 45,
    "nEdges": 903,
    "policy": "Edges are emitted only when the callee resolves …",
    "unparsedFiles": [
      // optional; [] means every scanned file parsed cleanly
      { "file": "app/legacy.py", "reason": "partial", "detail": "…" },
    ],
  },
  "nodes": {
    "app.main.main": {
      // internal node (kind: "internal")
      "id": "app.main.main",
      "name": "main",
      "qualname": "main",
      "file": "ingest/collector.go",
      "lineStart": 4,
      "lineEnd": 9,
      "signature": "def main()",
      "isAsync": false,
      "isMethod": false,
      "className": null,
      "decorators": [],
      "kind": "internal",
      "synthetic": false, // true = implied node (e.g. implicit constructor)
      "selfAttrsRead": [],
      "selfAttrsWritten": [],
      "paramTypes": {},
      "nCallees": 3,
      "nCallers": 0,
    },
    "boundary:os.getpid": {
      // boundary node (kind: "boundary")
      "id": "boundary:os.getpid",
      "name": "getpid",
      "qualname": "os.getpid",
      "module": "os",
      "className": "",
      "kind": "boundary",
      "nCallees": 0,
      "nCallers": 1,
    },
  },
  "edges": [
    {
      "callerId": "app.main.main",
      "calleeId": "ingest.collector.Source.Next",
      "isAwait": false,
      "callType": "internal_constructor",
      "line": 6,
      "raw": "c.source.Next",
    },
  ],
  "selfAttrs": { "Collector": { "dropped": { "readIn": ["…"], "writtenIn": ["…"] } } },
}

callTypeself_method · self_attr_method · param_method · internal_func · internal_constructor · boundary · boundary_constructor (कभी unresolved नहीं — वे dropped-calls.json में रहते हैं)।

phase1/dropped-calls.json

{
  "version": 1,
  "metadata": {
    "generatedAt": "…",
    "totalDropped": 12,
    "byCategory": { "builtin": 7, "bare_name": 3, "local_var_method": 2 },
  },
  "edgesByCategory": {
    "builtin": [
      { "caller": "app.main.main", "calleeRaw": "print", "isAwait": false, "line": 9, "raw": "print" },
    ],
  },
}

श्रेणियाँ: inherited_method, self_attr_unknown, string_literal_method, builtin, local_var_method, bare_name

phase1/scan-coverage.json

dropped-calls.json की सगी बहन, एक स्तर ऊपर: वह फ़ाइल हर उस call का हिसाब रखती है जिसका अनुमान लगाने से analyzer ने इनकार किया, और यह हर उस फ़ाइल का, जिसे विश्लेषित कर लेने का दावा करने से उसने इनकार किया।

{
  "version": 1,
  "metadata": {
    "generatedAt": "…",
    "nScanned": 412, // files that reached the graph — i.e. graph.metadata.scannedFiles
    "nUnparsed": 3,
    "byReason": { "partial": 1, "unparsable": 1, "unreadable": 1 },
  },
  "files": [
    // sorted by path, so an unchanged tree re-runs byte-identically
    { "file": "app/legacy.py", "reason": "partial", "detail": "the parse tree contains syntax errors…" },
    { "file": "ops/legacy.sh", "reason": "unparsable", "detail": "resolved is not a function" },
    { "file": "vendor/dangling.py", "reason": "unreadable", "detail": "ENOENT: no such file or directory…" },
  ],
}
reasonParser को क्या मिलाscannedFiles में?Card मिलता है?
unreadableकुछ नहीं — पढ़ना विफल हुआ
unparsableकुछ नहीं — grammar ने throw किया
partialअसली facts, पर अधूरे
  • unreadable — discovery ने रास्ता सूचीबद्ध तो किया, पर पढ़ना विफल रहा: कोई permission mode, कोई dangling symlink, या ऐसी फ़ाइल जिसे build ने run के नीचे से मिटा दिया। detail में errno का संदेश रहता है।
  • unparsable — grammar ने throw किया, या कोई tree लौटाया ही नहीं। शून्य facts। आज case वाली shell स्क्रिप्ट यही पैदा करती है।
  • partial — फ़ाइल parse तो हुई, पर rootNode.hasError: tree-sitter ने जो पाठ उसकी समझ में नहीं आया उसे एक error node में टिका दिया और आगे बढ़ गया। बाक़ी फ़ाइल से निकाली गई हर चीज़ असली है — ग़ायब बस वही है जो उस node के भीतर बैठा था, और बाहर से वह दिखता ही नहीं। इसी वजह से किसी पृष्ठ पर भरोसा करने से पहले फ़ाइल खुद पढ़ लेनी चाहिए।

ख़ाली files array एक सकारात्मक दावा है — "हर scanned फ़ाइल साफ़-साफ़ parse हुई"। जबकि artifact का न होना यह बताता है कि विश्लेषण इस record से पुराना है, और यह वही बात नहीं है।

पहली दो scannedFiles से क्यों हटा दी जाती हैं

जिस फ़ाइल से कोई facts नहीं निकले, वह पहले scannedFiles में बनी रहती थी — तो phase 2a उसका एक card लिखता और _coverage.json उसे described गिन लेता। इसके बाद handbook एक parser fact के तौर पर यह दावा करता कि जिस फ़ाइल को किसी ने पढ़ा तक नहीं, उसमें शून्य functions हैं। उन रास्तों को यहाँ हटा देने से हर सूची का एक ही मतलब रहता है: scannedFiles वह है जो analyzer ने पढ़ा, scan-coverage.json वह जो वह नहीं पढ़ सका।

phase2/cards/<rel>.json — FileCard

{
  "version": 1,
  "file": "ingest/collector.go",
  "purpose": "Drains the queue and executes each task.", // "" = generation failed (backfilled)
  "role": "domain_logic", // entrypoint|orchestration|domain_logic|io_transport|data_model|config|util|test|generated|other
  "lifecycle": "main loop", // free-form short hint; "none" when not meaningful
  "description": "…120-300 words…", // deep mode only
  "functions": [
    // deep mode only; facts from the graph, prose from the LLM
    {
      "id": "app.worker.Worker.run",
      "qualname": "Worker.run",
      "name": "run",
      "className": "Worker",
      "lineRange": [10, 13],
      "signature": "def run(self)",
      "calls": ["ingest.collector.valid"],
      "calledBy": ["app.main.main"],
      "extCalls": [],
      "nCalls": 3,
      "nCalledBy": 1,
      "nExtCalls": 0,
      "purpose": "…",
      "dataFlow": "…",
      "relations": "…", // may be empty; facts never are
    },
  ],
}

phase2/skeleton.yaml — Skeleton

metadata:
  version: 1
  archetype: demo task runner # one-phrase system shape
  draftedBy: skeleton-synth # skeleton-synth | skeleton-doctor | user
stages:
  - id:
      stage-1 # any filename-safe id (^[A-Za-z0-9][A-Za-z0-9._-]*$);
      # conventionally stage-N / stage-N.M / crosscut-N.
      # Reserved page names (overview, index, register(s), …)
      # are auto-suffixed by the normalizer.
    title: Startup
    description: Entry point wiring…
    parent: null # substages point at their parent id
    children: [stage-1.1] # derived; always rebuilt from parent on load
    crosscut: false # true = cross-cutting infrastructure

ठीक यही schema आप --strategy member / --skeleton के लिए हाथ से लिखते हैं। children छोड़ा जा सकता है या बासी हो सकता है — लोड करते समय वह सामान्यीकृत हो जाता है।

phase2/assignment.json — Assignment

{
  "version": 1,
  "fileStage": { "ingest/collector.go": { "stage": "stage-1", "also": [] } }, // "unassigned" allowed
  "buckets": { "stage-1": ["ingest/collector.go"] }, // primary stage only; disjoint
  "coverage": { "nFiles": 5, "nAssigned": 5, "unassigned": [] },
}

phase2/organization.yaml — Organization

metadata: { version: 1, nStages: 4 }
stages:
  stage-2:
    title: Task execution
    groups:
      - title: Core flow
        summary: Everything this stage owns, in execution order.
        files:
          - { file: ingest/collector.go, purpose: '…', role: domain_logic, nFunctions: 5 }
    orderedFiles: [ingest/collector.go, ingest/http_source.go] # flat reading order across groups
coverage: { nFiles: 5, nOrganized: 5 }

phase3/narration.json — Narration

{
  "version": 1,
  "lang": "en", // en | zh
  "systemOverview": "…200-350 words…",
  "stageSummaries": { "stage-1": "…100-200 words…" },
}

phase3/registers.json — Registers

{
  "version": 1,
  "registers": [
    {
      "id": "reg-task-queue", // ^reg-[a-z0-9-]+$
      "semantics": "The FIFO list of pending tasks…",
      "stages": ["stage-1", "stage-2"],
    }, // only real stage ids
  ],
}

रेंडर किया गया handbook (handbook render)

<out>/
  overview.md          H1 title + 🗺️ system overview + see-also links
  index.md             recursive stage index (heading depth = tree depth)
  register.md          | State register | Semantics | Stages touched |  (only when registers exist)
  <sid>.md             one page per content-bearing stage (summary, sub-stages,
                       organization groups, per-file cards with function details,
                       📊 state-registers section when touched)
  agent/               (--agent-site) index.md · symbols.tsv · files.tsv · calls.tsv · stages/<sid>.md
  html/                (--html) self-contained multi-page site (no external requests)
  handbook.html        (--html-single) one self-contained page

Agent index (--agent-site)

<out>/agent/
  index.md            the only file meant to be read whole: lookup recipes, the stage
                      table, the register table, coverage
  symbols.tsv         name → path:startLine-endLine, kind, stage, nCalledBy, signature
  files.tsv           path → stage, role, nSymbols, purpose[prose]
  calls.tsv           call edges: the caller always located, the callee located or
                      marked boundary:<import specifier>
  stages/<sid>.md     second hop: the stage's file list and its co-change pairs

मानवीय artifact समझाता है; agent artifact पता बताता है। ये एक ही पाठ के दो रूप नहीं हैं। जहाँ किसी agent को व्याख्या चाहिए, वह वहाँ से सिर्फ़ एक hop दूर है — हर stage पृष्ठ उसकी नक़ल करने के बजाय मानवीय पृष्ठ से जोड़ देता है।

TSV क्यों, markdown तालिकाएँ क्यों नहीं

  • Markdown तालिका इस repo की 338 signature पंक्तियों को चुपचाप बिगाड़ देती, क्योंकि TypeScript union type में | होता है। Tab स्रोत पाठ से नहीं टकराता।
  • प्रति पंक्ति एक तथ्य truncation के बाद भी बचा रहता है। हर grep नुस्ख़ा एक ही पंक्ति में पूरा जवाब लौटाता है — नाम, स्थान, kind, stage, callers और signature, सब साथ — इसलिए कटा हुआ नतीजा भी काम का रहता है।
  • एक tab पूरे column को बाँध देता है: grep "^scan\t" सिर्फ़ scan नाम वाले symbol से मेल खाता है, उस शब्द वाली हर पंक्ति से नहीं।

Columns का क्रम मूल्य का क्रम है, गद्य सबसे आख़िर में, ताकि लंबी पंक्तियाँ काटने वाला कोई consumer किसी path से पहले गद्य खाए।

Header पंक्तियाँ

हर तालिका # वाली comment पंक्तियों से शुरू होती है, जो columns और भरोसे की सीमा — दोनों बताती हैं। यह वही खुलासा है जो pipeline हर जगह करती है, बस उसी artifact पर ले आया गया जो उसे ढो रहा है:

# name	location	kind	stage	nCalledBy	signature
# parser facts. kind=fn is a function or method. kind=type:<class|interface|struct|record|enum|
# trait|alias|other> is a parsed type DECLARATION, span read off the declaration itself.
# kind=class-derived is the fallback where a language's adapter extracts no types: the SPAN is
# min..max of the class's METHODS, not of the declaration. Which languages are indexed and which
# fall back is stated in index.md under "coverage" — a miss here is not proof a name does not exist.
# nCalledBy counts callers inside the scanned set PLUS callers that reach it through an import
# (see calls.tsv boundary rows); a cross-package-only callee would otherwise read as dead code.

calls.tsv इससे मेल खाती बात कहती है, और यह भी बताती है कि उसमें दो तरह की rows का फ़र्क़ क्या है:

# callerQualname	callerLocation	calleeQualname	calleeLocation
# calleeLocation is path:line when the analyzer resolved it, or boundary:<import specifier>
# when the call leaves the scanned set — the name is known, the location is not and is not guessed.
# A call the analyzer could not pin down at all is in phase1/dropped-calls.json,
# never guessed here — so absence is not proof nothing calls it.

Boundary edges, और monorepo को इनकी ज़रूरत क्यों है

जो call किसी import के ज़रिए scan किए गए दायरे से बाहर जाती है, उसके callee की जगह boundary:<specifier> लिखी जाती है — कभी कोई path नहीं। नाम एक तथ्य है; जगह नहीं है, और उसका अनुमान नहीं लगाया जाता।

monorepo में यह कोई footnote नहीं है — यही वह हिस्सा है जो एक agent सबसे ज़्यादा जानना चाहता है। इस repository पर मापा गया: 3,565 edges में से 1,063 boundary edges हैं, जिनमें 284 @handbooks/core की ओर जाती हैं। सिर्फ़ resolved edges के साथ checkLanguage — जिसे दूसरे package से चार बार बुलाया जाता है — शून्य callers के साथ दिखता था, और agent उसे dead code पढ़ता है। वह एक ग़लत pointer है, कोई कमी नहीं, और ग़लत pointer ही वह विफलता है जिसे रोकने के लिए यह artifact मौजूद है।

इसी कारण symbols.tsv का nCalledBy boundary callers को भी package-भीतर के callers के साथ गिनता है, और उसका header यह कहता भी है। boundary: को path समझा ही नहीं जा सकता, इसलिए उन्हें शामिल करने से कुछ गढ़ा नहीं जाता।

Type rows, और उनके नीचे का fallback

symbols.tsv में तीन तरह की rows हैं। fn एक function या method है। type:<kind> एक parse किया गया type declaration है — span declaration से ही पढ़ा जाता है — और यह एक बंद शब्दावली से आता है: class, interface, struct, record, enum, trait, alias, otherrecord को struct में नहीं मिलाया गया, क्योंकि Java या C# का record एक reference type है, और इस शब्दावली में struct ही वह शब्द है जिसका मतलब value type भी है। other कूड़ेदान नहीं बल्कि भार उठाने वाला है: Go का defined type (type Celsius float64) alias नहीं है, Rust का union struct नहीं है, Java का @interface interface नहीं है — और signature declaration को जैसा लिखा है वैसा रखता है, इसलिए native keyword कभी खोता नहीं।

कौन सी भाषाएँ असल में types निकालती हैं, यह per adapter घोषित है और index.md में disclose किया जाता है — ठीक वैसे ही जैसे analysis fidelity (invariant 3)। AdapterCapabilities.typeKinds boolean नहीं बल्कि एक list है, क्योंकि कोई adapter classes पा सकता है और हर interface चूक सकता है; [] एक सकारात्मक दावा है, और field के अनुपस्थित होने का अर्थ है कि artifact इससे पुराना है — उसे unknown बताया जाता है, कभी शून्य नहीं।

सभी बारह precisely-parsed भाषाएँ इन्हें निकालती हैं — C++, C#, Dart, Go, Java, PHP, Python, Ruby, Rust, Solidity, Swift, TypeScript। Shell [] घोषित करता है क्योंकि उसमें type declaration ही नहीं होते। पाँच generic-tier भाषाएँ (Kotlin, Objective-C, OCaml, Scala, Zig) जान-बूझकर [] घोषित करती हैं: उनका adapter patterns मिलाता है, precisely parse नहीं करता, इसलिए उनसे आई type row IR में precisely-parsed row से अलग पहचानी ही नहीं जा सकेगी, जबकि उसकी fidelity एक दर्जा नीचे है — ठीक वही चीज़ जिसे रोकने के लिए invariant 3 है। उनके लिए class-derived fallback बना रहता है।

असली repositories पर मापा गया, rows की तुलना उन declarations से जो grep देख सकता है: PHP और Solidity 100%, C# 98.9%, Swift 97.0%, Dart 96.1%, Ruby 92.7%, C++ 87.5% (केवल उन files में जो साफ़ parse हुईं; spdlog के macro-भारी headers grammar को ही हरा देते हैं, जो scan-coverage.json दर्ज करता है)। हर कमी एक ऐसा declaration है जिस पर adapter ने अनुमान लगाने से इनकार किया — function body के भीतर घोषित type, या arity-मुक्त id model में टकराने वाला नाम — कभी कोई अनुमानित span नहीं।

class-derived वह fallback है जहाँ adapter कोई type नहीं निकालता: span उस class के methods का min…max है — यानी members कहाँ हैं, declaration कहाँ है वह नहीं — इसलिए उसे label किया जाता है, parse किया हुआ तथ्य बताकर पेश नहीं किया जाता। इस repository पर असली type extraction जोड़ने से class-derived 45 rows से 19 पर आ गया, और बची हुई हर row एक object literal है, type declaration नहीं — fallback को ठीक यही पकड़ना चाहिए।

Declaration का अपना span लेने की एक कीमत: जब किसी declaration के आगे attribute या annotation हो, तो span वहीं से शुरू होता है — क्योंकि grammar का node वहीं से शुरू होता है। Signature को इससे बचाया गया है: अगर cap type का नाम काट देगा, तो उसके बजाय attributes हटा दिए जाते हैं और आगे एक लगता है, क्योंकि जो signature यह नहीं बताता कि वह क्या declare करता है, वह छोटा signature नहीं बल्कि बेकार signature है।

Disclosure coverage से ज़्यादा मायने रखता है: एक agent किसी type नाम को grep करे, कुछ न पाए, और मान ले कि वह type मौजूद नहीं है — यही वह ग़लत pointer है जिसे रोकने के लिए यह artifact है। Constants, variables और macros किसी भी भाषा में index नहीं होते, और index.md यह कहता है।

ताज़गी

index.md का header HandbookModel.provenance ढोता है — { commit?, generatedAt }, जो run manifest से पढ़ा जाता है। अब मुख्य payload line numbers हैं, और बासी line number ही वह इकलौता तथ्य है जो चुपचाप ग़लत होता है, इसलिए artifact ख़ुद बताता है कि वह कब और किसके सापेक्ष बना।

SKILL पैकेज (handbook skill)

<out>/
  SKILL.md                       frontmatter: name (<slug>-handbook) + description
                                 ("Use when … Do not use …"); body = routing protocol
  references/
    overview.md  index.md  registers.md
    stages/<sid>.md
    agent/                       (--agent-dir) index.md · symbols.tsv · files.tsv ·
                                 calls.tsv · stages/<sid>.md
    coverage.json                (optional) {schemaVersion, summary, files:[{path,stage,sha256}]}

सत्यापन अनुबंध (handbook validate): frontmatter में ठीक name + description हों; description बताए कि कब इस्तेमाल करें और कब नहीं; मुख्य भाग references/index.md का हवाला दे और असली स्रोत की ओर भेजे; overview/index/registers/stages मौजूद हों; index हर stage पृष्ठ से जोड़े; coverage में कोई दोहरा रास्ता न हो; --source के साथ hashes जीवित वृक्ष से मेल खाएँ। references/agent/ directory वैकल्पिक है, पर जब वह मौजूद हो तो उसमें index.md और तीनों तालिकाएँ होनी ही चाहिए — index और उसकी fact तालिकाएँ या तो साथ जाती हैं या बिल्कुल नहीं।

Planner का आउटपुट (handbook plan)

एक markdown योजना: गद्य सारांश → EDIT ब्लॉक → एक declarations JSON ब्लॉक।

### EDIT 1

- file: `app/engine.py`
- where: `Engine.spin (~5)` — add retry

```old
<byte-exact current text, ≥3 context lines each side, unique in the file>
```

```new
<replacement text>
```

```json
{ "will_modify": ["Engine.spin"], "will_add": [], "will_remove": [] }
```

Resync case directory (handbook resync --case)

<case>/
  edited/               the changed source tree            (required)
  plan.md               change description; its ```json declarations block
                        (will_modify/will_add/will_remove) sharpens scope (optional)
  change.diff           unified diff; PRESENT AND EMPTY = "nothing to resync" (optional)
  resync-report.json    written by resync: {skipped, changedFiles, addedFiles,
                        deletedFiles, affectedStages, cardsRegenerated, narrated}

इस पृष्ठ पर