{
  "metadata": {
    "skill_name": "wiki",
    "skill_path": "<path/to/skill>",
    "executor_model": "<model-name>",
    "analyzer_model": "<model-name>",
    "timestamp": "2026-04-14T18:54:21Z",
    "evals_run": [
      1,
      2,
      3,
      4,
      5
    ],
    "runs_per_configuration": 3
  },
  "runs": [
    {
      "eval_id": 1,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 12,
        "failed": 0,
        "total": 12,
        "time_seconds": 389.3,
        "tokens": 66797,
        "tool_calls": 0,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Runs init_wiki.js to create directory scaffold under the project root",
          "passed": true,
          "evidence": "events.jsonl line 1 op='init' records created_dirs=[wiki, wiki/claims, wiki/synthesis, wiki/templates, raw, graph, audit/open, audit/resolved, log/daily, outputs/queries, outputs/reports, .wiki-cache] and created_files=[wiki.config.yaml, wiki/index.md, wiki/summaries.md, wiki/overview.md, .wiki-ignore, log/events.jsonl, graph/edges.jsonl, .claude/settings.json]. Scaffold is materialized on disk under with_skill/outputs/."
        },
        {
          "text": "wiki.config.yaml is created with wiki.domain: 'backend-services'",
          "passed": true,
          "evidence": "outputs/wiki.config.yaml line 3: 'domain: backend-services' under the top-level 'wiki:' block (line 1)."
        },
        {
          "text": "Directory scaffold includes wiki/, raw/, graph/, audit/, log/, outputs/",
          "passed": true,
          "evidence": "ls -d confirms all six directories exist under with_skill/outputs/ (audit, graph, log, outputs, raw, wiki)."
        },
        {
          "text": "Source file is copied to raw/ (path preserved relative to the ingested root)",
          "passed": true,
          "evidence": "outputs/raw/docs/architecture.md exists (3449 bytes) preserving the docs/ prefix from the source path 'docs/architecture.md'. events.jsonl op='ingest' records raw_copy='raw/docs/architecture.md'. File content is the full architecture.md source (sha256 251db84f...)."
        },
        {
          "text": "Generated wiki page has frontmatter with non-empty title, type set to 'concept' (or one of concept|index|synthesis|claim|template), sources listing the ingested file, created + updated ISO dates, a non-empty summary, and 4-8 content-only tags",
          "passed": true,
          "evidence": "All 5 generated pages have conforming frontmatter. e.g. wiki/authentication/jwt.md: title='JWT Access Tokens (RS256)', type=concept, tags=[jwt, rs256, authentication, asymmetric-signing, access-tokens] (5 tags), sources=[raw/docs/architecture.md], created/updated=2026-04-14, multi-line summary. overview.md type=index, claims/logout-revokes-all-tokens.md type=claim — all within the allowed enum."
        },
        {
          "text": "Tags are content-only concepts (e.g., 'jwt', 'authentication', 'session-management') — no structural/temporal/metadata tags",
          "passed": true,
          "evidence": "Aggregated tag set across all 5 pages: jwt, rs256, authentication, asymmetric-signing, access-tokens, session-management, refresh-tokens, postgresql, cookies, revocation, token-rotation, logout. All are domain concepts — no structural (e.g. 'page', 'index'), temporal ('2026', 'new'), or metadata ('draft', 'stub') tags are present."
        },
        {
          "text": "summaries.md has a non-stub entry (≥ 20 chars) for the new page",
          "passed": true,
          "evidence": "wiki/summaries.md is 2311 bytes and contains multi-sentence summaries for each of the 5 generated pages (e.g. JWT entry: 'Every authenticated API call carries a short-lived JWT in the Authorization: Bearer header...'), each well over 20 characters."
        },
        {
          "text": "Query answer includes at least two inline markdown links and each link resolves to a file that actually exists under wiki/",
          "passed": true,
          "evidence": "outputs/queries/2026-04-14-how-does-authentication-work.md contains 5 inline links: ../../wiki/authentication/jwt.md, ../../wiki/authentication/sessions.md, ../../wiki/authentication/refresh.md, ../../wiki/claims/logout-revokes-all-tokens.md, ../../wiki/authentication/overview.md. Bash [ -f ] test from the queries/ directory confirmed all 5 targets EXIST."
        },
        {
          "text": "Query answer mentions at least two concrete facts from the source (e.g., JWT, sessions table, refresh endpoint) — hallucinated answers that cite nonexistent pages must fail",
          "passed": true,
          "evidence": "Answer cites specific source facts: 'signed with RS256' (source §1), 'RSA key (2048 bits)' (§1), 'Access tokens live 15 minutes' (§1), custom 'sid' claim (§1), PostgreSQL 'sessions' table with session_id UUID, HttpOnly/Secure/SameSite=Lax cookie, '(user_id, revoked_at)' index, 'POST /auth/refresh' (§3), refresh_tokens table with token_hash/session_id/rotated_to (§3), 'revoked_at = now()' on logout (§3). All match architecture.md verbatim; no fabricated pages cited."
        },
        {
          "text": "Answer is archived to outputs/queries/ as a dated .md file",
          "passed": true,
          "evidence": "outputs/outputs/queries/2026-04-14-how-does-authentication-work.md exists (4458 bytes). Filename begins with today's ISO date 2026-04-14. events.jsonl op='query' records archive_path='outputs/queries/2026-04-14-how-does-authentication-work.md'."
        },
        {
          "text": "events.jsonl records separate entries with op='init', op='ingest', and op='query' (one per operation), all with ISO timestamps",
          "passed": true,
          "evidence": "jq over outputs/log/events.jsonl yields exactly three records: {ts:'2026-04-14T18:24:03.875000+00:00', op:'init'}, {ts:'2026-04-14T18:27:26.011000+00:00', op:'ingest'}, {ts:'2026-04-14T18:28:42.005000+00:00', op:'query'}. The hardener (op='init' now logged) holds."
        },
        {
          "text": "graph/edges.jsonl contains at least one edge with a valid provenance tag (EXTRACTED, INFERRED, or AMBIGUOUS)",
          "passed": true,
          "evidence": "outputs/graph/edges.jsonl contains 9 edges; provenance distribution from jq: 8 EXTRACTED, 1 INFERRED. All values are in the allowed set {EXTRACTED, INFERRED, AMBIGUOUS}. Example edge: {from:'wiki/authentication/jwt.md', to:'wiki/authentication/sessions.md', type:'extends', provenance:'EXTRACTED'}."
        }
      ],
      "notes": []
    },
    {
      "eval_id": 2,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 6,
        "failed": 0,
        "total": 6,
        "time_seconds": 295.9,
        "tokens": 62565,
        "tool_calls": 0,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Q&A covers detection markers (class inheritance + __table__), file patterns, entity extraction (class_pattern, table_pattern, column_pattern), and at least one relationship pattern",
          "passed": true,
          "evidence": "qa-transcript.md Turn 1 section 1 covers file patterns ('`detection.file_patterns` (glob — e.g. `**/*.py`...'); section 2 covers detection markers ('base class (BaseModel, Entity, etc.)', '__table__', '__columns__'); section 3 covers entity extraction naming class_pattern, table_pattern, and column_pattern explicitly; section 4 covers relationships ('one_to_many, many_to_one, many_to_many, foreign_key' with concrete ForeignKey example). Turn 2 answers all with concrete values and the final YAML has a ForeignKey\\\\( relationship pattern."
        },
        {
          "text": "Q&A covers naming conventions (table_from_class and column_from_field)",
          "passed": true,
          "evidence": "qa-transcript.md Turn 1 section 5 explicitly asks for both keys: 'table_from_class: lower_case | snake_case | snake_case_plural' and 'column_from_field: typically snake_case'. Turn 2 answers both: 'tables are plural snake_case (User → users). Columns are already snake_case in source so column_from_field: snake_case'."
        },
        {
          "text": "Generated YAML has every required top-level field: name, language, description, detection, entity_extraction, relationship_patterns, naming_conventions",
          "passed": true,
          "evidence": "yaml.load on custom-basemodel.yaml yields top-level keys ['name','language','description','detection','entity_extraction','relationship_detection','naming_conventions']. The YAML follows the canonical shipped schema (matches sqlalchemy.yaml/django.yaml exactly) where relationship patterns are nested under relationship_detection.patterns; the loader surfaces this as profile.relationship_patterns (confirmed in profile-load-test.txt line 12: 'relationship_patterns: OK ([{\"pattern\":\"ForeignKey\\\\(\",\"type\":\"foreign_key\"}])'). All seven required semantic fields are present and populated."
        },
        {
          "text": "entity_extraction.class_pattern has a single capture group that matches at least one class name in example.py (verified by running the pattern against the fixture)",
          "passed": true,
          "evidence": "Ran `new RegExp('class\\\\s+(\\\\w+)\\\\s*\\\\(\\\\s*BaseModel\\\\s*\\\\)', 'gm')` against example.py: 3 matches, each with exactly one capture group. Captured names: 'User' (index 241), 'Post' (index 338), 'Comment' (index 501). m.length-1 === 1 for all matches, confirming single capture group. Helper classes BaseModel and ForeignKey are correctly excluded (neither inherits from BaseModel)."
        },
        {
          "text": "profile loader (profiles.js loadProfile) accepts the YAML without raising ProfileValueError",
          "passed": true,
          "evidence": "Executed `loadProfile('/Users/narayan/src/doc-wiki/wiki-workspace/iteration-3/orm-agent-custom-profile/with_skill/outputs/custom-basemodel.yaml')` — returned a populated OrmProfile with keys [name, language, description, file_patterns, markers, class_pattern, table_pattern, column_pattern, relationship_patterns, naming_conventions]. No exception thrown. Matches the claim in profile-load-test.txt."
        },
        {
          "text": "When the profile is applied to example.py, extractEntities yields at least one ExtractedEntity per BaseModel class in the fixture — not all classes collapsing to a single table",
          "passed": true,
          "evidence": "Ran `extractEntities({[examplePath]: content}, profile)` and got 3 entities: User/table_name=users, Post/table_name=posts, Comment/table_name=comments. new Set([users,posts,comments]).size === 3 — all DIFFERENT, each matching the corresponding __table__ literal in the fixture. The profile's deliberate __tablename__ vs __table__ mismatch forces the classToTable fallback (snake_case_plural), producing per-class tables without collapse."
        }
      ],
      "notes": [
        "column_pattern is not scoped to __columns__ list literals; it matches every quoted identifier in the source file, producing identical ~20-entry column lists across all three entities. This was not caught by any assertion.",
        "Deliberately non-matching table_pattern (`__tablename__` vs fixture's `__table__`) forces every entity through the naming-convention fallback. This is an acknowledged trade-off documented in qa-transcript.md Turn 2 and the YAML description."
      ]
    },
    {
      "eval_id": 1,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 6,
        "failed": 0,
        "total": 6,
        "time_seconds": 147.2,
        "tokens": 38891,
        "tool_calls": 0,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Target page emits a fenced ```mermaid code block containing the erDiagram directive",
          "passed": true,
          "evidence": "target-after.md lines 9-16 show a ```mermaid fence opening on line 9 with `erDiagram` directive on line 10, closed by ``` on line 16."
        },
        {
          "text": "The mermaid block lives BETWEEN `<!-- wiki-mermaid: start -->` and `<!-- wiki-mermaid: end -->` on the target page — marker positions are preserved, content between them is replaced. EOF append is a contract violation when markers are present.",
          "passed": true,
          "evidence": "marker-injection-check.md reports byte offsets: start marker at 98, fence-open at 144, fence-close at 359, end marker at 363, EOF at 435. 144 > 98 and 359 < 363, so the fence is strictly between markers. Tail of file is `rmaid: end -->\\nTrailing paragraph that must not be touched.\\n` — fence is not appended at EOF. File length 435 bytes verified independently with `wc -c`."
        },
        {
          "text": "erDiagram contains all three entity names (User, Order, Product) and each has at least one attribute from the input JSON (e.g., id, email for User)",
          "passed": true,
          "evidence": "target-after.md lines 13-15: `User { int id PK string email }`, `Order { int id PK int user_id FK decimal total }`, `Product { int id PK string name }`. All three entities present with attributes matching input.json verbatim."
        },
        {
          "text": "Uses `||--o{` for one-to-many edges (matching the input's cardinality)",
          "passed": true,
          "evidence": "target-after.md line 11: `User ||--o{ Order : places` — `||--o{` token emitted for the one-to-many User→Order edge from input.json."
        },
        {
          "text": "Content outside the markers is preserved byte-for-byte (diff of before vs after, excluding the region between markers, is empty)",
          "passed": true,
          "evidence": "preamble-check.md reports sha256(ABOVE_before)==sha256(ABOVE_after)==`9180401658c083506007dcf8858e3422e1a07da3421b462ede89c612c856e0d5` and sha256(BELOW_before)==sha256(BELOW_after)==`f3f03f0984e016dc099ef401dd22b5f3f19a0b74559fb9eed18113686a8f720e`. Full-file hashes `85de199d...` (before) and `56ca914c...` (after) independently verified via shasum. Only the between-markers region changed (21→237 bytes)."
        },
        {
          "text": "mermaid_lint.js reports no issues on the resulting page",
          "passed": true,
          "evidence": "mermaid-lint.txt contents: `[]` followed by `exit:0` — empty issues array with clean exit status."
        }
      ],
      "notes": []
    },
    {
      "eval_id": 2,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 6,
        "failed": 0,
        "total": 6,
        "time_seconds": 194.2,
        "tokens": 43829,
        "tool_calls": 0,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Mermaid block uses flowchart directive (flowchart TD|TB|LR|RL)",
          "passed": true,
          "evidence": "All three post-run snapshots (pipeline-after-run1.md, -run2.md, -run3.md) contain `flowchart TD` on the first code line inside the mermaid fence, verified by independent grep. Input JSON also declares type='flowchart TD'."
        },
        {
          "text": "Labels containing special characters (quotes, angle brackets) are escaped so mermaid_lint passes — a single escape case is OK but the lint must be clean",
          "passed": true,
          "evidence": "mermaid-lint.txt shows `[]` and `rc=0` for all three post-run snapshots. Caveat: the inputs in input-1.json and input-2.json use only bare identifiers (checkout, build, test, docker, push, deploy, smoke, notify) with no quotes or angle brackets, so the escaping path was never actually exercised. The assertion 'lint must be clean' holds; the 'escape' half of the assertion was not tested by these inputs."
        },
        {
          "text": "Re-running with the IDENTICAL input produces a byte-identical pipeline.md (true idempotency)",
          "passed": true,
          "evidence": "Independently recomputed SHA-256: pipeline-after-run1.md == pipeline-after-run3.md == 8a41460e5f1754671019cbb3745ba37887ed2ff28c9f739e4b20d74b102ba2c4. `diff -u pipeline-after-run1.md pipeline-after-run3.md` returns exit 0 with no output. Run 3 used the same input-1.json after an intervening run 2 with input-2, so the generator is a pure function of current input, not history."
        },
        {
          "text": "Re-running with a DIFFERENT input (e.g., an added node) REPLACES the previous block between the markers — not appends a second block; pipeline.md still contains exactly one `<!-- wiki-mermaid: start -->` and one `<!-- wiki-mermaid: end -->` after both runs",
          "passed": true,
          "evidence": "Marker counts recomputed independently: every snapshot (before + 3 runs) shows exactly 1 start and 1 end marker. replace-check.md diff confirms every changed line between run1 and run2 lives strictly between the markers — input-2 added the `test` and `notify` nodes, which appear in the replaced block, and no second mermaid fence was introduced."
        },
        {
          "text": "Only the named target file is modified — no other .md files in the wiki change across the runs",
          "passed": true,
          "evidence": "scope-check.md documents a decoy `unrelated.md` in the same working directory containing its own wiki-mermaid markers; its SHA-256 is constant (`43fea567c596ae67d4273581b754365e2037b166943358b75e499f18c730b781`) across before, run 1, run 2, and run 3. No new .md files appeared."
        },
        {
          "text": "Content outside the markers on pipeline.md is preserved byte-for-byte across both runs",
          "passed": true,
          "evidence": "Independently recomputed an outside-markers slice (everything up to and including the start marker plus everything from the end marker onward) for all four snapshots: all share sha256 prefix e487fb98a1aca176 and length 263 bytes. The frontmatter, `# Deploy Pipeline` heading, intro prose, and `## Operational notes` section are preserved byte-for-byte across every run. (Note: the report's replace-check.md uses a slightly different slice definition that excludes the markers themselves — hash prefix d01a9c64c13159eb, 237 bytes — but its all-four-equal conclusion is the same.)"
        }
      ],
      "notes": []
    },
    {
      "eval_id": 1,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 6,
        "failed": 0,
        "total": 6,
        "time_seconds": 274.3,
        "tokens": 53170,
        "tool_calls": null,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Every generated CLAUDE.md contains exactly one `<!-- wiki-managed: start -->` and one `<!-- wiki-managed: end -->` marker (grep -c returns 1 for each on every file)",
          "passed": true,
          "evidence": "Verified directly on live fixture via grep -c. For each of the 4 files (/tmp/eval-i3-cmd-gen/CLAUDE.md and the three services/*/CLAUDE.md), start=1 end=1. marker-count-check.md reports the same counts."
        },
        {
          "text": "Root CLAUDE.md lists EXACTLY three submodule links — one per discovered submodule — with relative paths that resolve to real CLAUDE.md files under the submodule dirs",
          "passed": true,
          "evidence": "root-CLAUDE.md (and live /tmp/eval-i3-cmd-gen/CLAUDE.md) contains exactly 3 submodule links under '## Submodules': services/api, services/gateway, services/worker. Each target resolves to an existing CLAUDE.md (confirmed by directory listing: api/, gateway/, worker/ all have 418-byte CLAUDE.md files)."
        },
        {
          "text": "Each submodule CLAUDE.md contains a relative link back to the root CLAUDE.md (e.g., `../../CLAUDE.md`) under a Parent Project heading, and the link target resolves",
          "passed": true,
          "evidence": "All three submodule files (submodule-api-CLAUDE.md, submodule-gateway-CLAUDE.md, submodule-worker-CLAUDE.md) contain the '## Parent Project' heading immediately followed by '- [Root CLAUDE.md](../../CLAUDE.md)'. From services/{api,gateway,worker}/CLAUDE.md, '../../CLAUDE.md' resolves to /tmp/eval-i3-cmd-gen/CLAUDE.md, which exists."
        },
        {
          "text": "Managed section content is non-empty: each file's region between the start/end markers contains at least an Overview heading OR a non-empty link to wiki/index.md — empty markers fail",
          "passed": true,
          "evidence": "Every managed section contains both '## Overview' (line 2 or 3) AND '- [Wiki documentation](wiki/index.md)'. Root additionally has ## Submodules, ## Build & Run, ## Service Dependencies, ## Database References. Submodules additionally have ## Parent Project. managed-content-check.md confirms non-empty on all 4."
        },
        {
          "text": "Running the generator a second time produces byte-identical CLAUDE.md files across the board (diff -ru is empty) — idempotency holds",
          "passed": true,
          "evidence": "idempotency-check.md documents snapshot-then-rerun-then-diff procedure across all 4 files; all four diffs returned empty. Procedure used node agents/wiki-claude-md-agent/scripts/claude_md_gen.js with identical flags as Run 1."
        },
        {
          "text": "When the user edits content OUTSIDE the managed markers between runs (e.g., adds a new heading above the start marker), the next generation preserves that edit unchanged",
          "passed": true,
          "evidence": "adversarial-preserve.md documents inserting '<!-- user-added: testnote -->' above the start marker in root CLAUDE.md, re-running generator, and confirming the line survives byte-identical at line 1. Independently verified: live /tmp/eval-i3-cmd-gen/CLAUDE.md still contains '<!-- user-added: testnote -->' as line 1, above '<!-- wiki-managed: start -->' on line 2. The report correctly attributes this to _MANAGED_RE being a single-match replace over only the inter-marker region, with the G-CLAUDE-MD-MARKER guard refusing to mutate unbalanced files."
        }
      ],
      "notes": []
    },
    {
      "eval_id": 2,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 6,
        "failed": 0,
        "total": 6,
        "time_seconds": 182.9,
        "tokens": 45247,
        "tool_calls": null,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Bytes above the `<!-- wiki-managed: start -->` marker are preserved — sha256 of the pre-marker region before vs after matches",
          "passed": true,
          "evidence": "Independently recomputed pre-region SHA-256 on CLAUDE-before.md and CLAUDE-after.md: both are b8dffa0c78a6d083e2189c920731db64b447f9a1a71931f0504bcb8f560b7ce7 (52 bytes, exactly `# My Custom Header\\n\\nUser's manual intro paragraph.\\n\\n`). Matches preservation-hashes.md."
        },
        {
          "text": "Bytes below the `<!-- wiki-managed: end -->` marker are preserved — sha256 of the post-marker region before vs after matches",
          "passed": true,
          "evidence": "Independently recomputed post-region SHA-256 on both files: 2d83eb553a0c387f642c324f4f290f3aaae51c0335b699892441f933c1461556 (47 bytes, exactly `\\n\\n## Manual footer\\n\\nUser's manual footer note.\\n`). Matches preservation-hashes.md."
        },
        {
          "text": "The managed section is refreshed from the current wiki state — its sha256 differs between before and after (proving the tool actually ran)",
          "passed": true,
          "evidence": "Independent hashes: before managed-region sha256=6662eeddc77b517de904883a0d458bd348d0b3dec7b02303ff2e81963d480cfe (79 bytes, stub `(old stale placeholder)`); after managed-region sha256=8f7a965ec8bba334ac97b041e75b0b84369c018960d6a4012e6ff3c28f2a0ca5 (366 bytes, 4 real sections). Hashes differ, confirming regeneration occurred."
        },
        {
          "text": "The refreshed managed section contains substantive content: at least one heading AND either a link into wiki/ or a mention of the project's real name — a block of whitespace or an error stub fails this",
          "passed": true,
          "evidence": "Extraction found 4 level-2 headings (`## Overview`, `## Build & Run`, `## Service Dependencies`, `## Database References`) AND both disjuncts of the OR: a `[Wiki documentation](wiki/index.md)` link AND the project name `**eval-i3-cmd-preserve**`. Content is 366 bytes of structured markdown, no error stub."
        },
        {
          "text": "After regeneration, the file still contains exactly one `<!-- wiki-managed: start -->` and one `<!-- wiki-managed: end -->` marker (no marker duplication, no whitespace drift on the marker lines)",
          "passed": true,
          "evidence": "`grep -c` on CLAUDE-after.md returns 1 for each marker. `od -c` of the two marker lines shows exactly `<!-- wiki-managed: start -->\\n` and `<!-- wiki-managed: end -->\\n` at file lines 5 and 23 with no leading/trailing whitespace and no tabs. Matches marker-count-check.md."
        },
        {
          "text": "A second run against the same wiki state produces a byte-identical file (idempotent)",
          "passed": true,
          "evidence": "idempotency-check.md shows `diff -q CLAUDE.md CLAUDE.md.pass1` produced no output and both files have SHA-256 60b5fa5188ce501052405978db940ee95ad7b2b65fa8806969962512fa27deb5. I independently hashed CLAUDE-after.md and got the same digest 60b5fa5188ce501052405978db940ee95ad7b2b65fa8806969962512fa27deb5."
        }
      ],
      "notes": []
    },
    {
      "eval_id": 2,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 6,
        "failed": 0,
        "total": 6,
        "time_seconds": 249.3,
        "tokens": 80777,
        "tool_calls": null,
        "errors": 0
      },
      "expectations": [
        {
          "text": "lint_checks.js is invoked and its first-pass report lists at least one 'broken_links' violation and at least one 'missing_frontmatter' violation",
          "passed": true,
          "evidence": "lint-report-before.md contains the raw JSON produced by `node skills/wiki/scripts/lint_checks.js --wiki-root /tmp/eval-i3-lint-wiki`. It lists 2 broken_links error rows (page-a.md -> ./page-d.md; page-c.md -> ./ghost.md) and 3 missing_frontmatter error rows (page-b.md sources; page-b.md tags; page-c.md 'No frontmatter found'). Summary: error=5, warning=6, info=0."
        },
        {
          "text": "Each violation record names the offending file path",
          "passed": true,
          "evidence": "Every issue object in the JSON carries a `page` field with an absolute path, e.g. '/tmp/eval-i3-lint-wiki/wiki/page-a.md', '/tmp/eval-i3-lint-wiki/wiki/page-c.md', '/tmp/eval-i3-lint-wiki/wiki/page-b.md'. The 'Assertion-relevant subset' table in the report also reproduces the file path per violation."
        },
        {
          "text": "Auto-fix applies the frontmatter fields required by the linter (title, type, tags, sources, created, updated, summary) — running the linter again after fix reports zero errors",
          "passed": true,
          "evidence": "post-fix page-b.md now contains tags: [bravo, core] and sources: - internal/notes-b.md (added by auto-fix). post-fix page-c.md has a complete frontmatter block with all required fields: title, type, tags, sources, created, updated, quality, summary. lint-report-after.md shows summary {error: 0, warning: 7, info: 0} — zero errors; warnings are orphan_page / isolated_node only."
        },
        {
          "text": "Broken-link fixes either create a stub for the missing target OR remove the link, never rewrite the page body wholesale (original headings and primary prose are preserved byte-identically outside the fix region)",
          "passed": true,
          "evidence": "Verified by my own diff: page-a.md pre/post are byte-identical (broken link to page-d.md healed by creating a new stub post-fix-wiki/wiki/page-d.md, page-a body untouched). page-b.md body (post-frontmatter) is byte-identical pre/post. page-c.md diff shows exactly the expected fix-region changes: one leading blank line after the new frontmatter, and the single link `[missing](./ghost.md)` collapsed to the plain word `missing`; every other byte — title, paragraphs, 'Charlie is the third concept…' closing — is preserved. page-e.md (control) is byte-identical. This matches the preservation-check.md narrative and the events.jsonl actions (create_stub for page-d, remove_broken_link for ghost.md)."
        },
        {
          "text": "The fix action went through the wiki skill — events.jsonl records op='lint' AND op='fix' in order, each with target_page paths",
          "passed": true,
          "evidence": "events.jsonl line 1: ts 2026-04-14T18:18:48 op='lint' target_pages=[page-a.md, page-b.md, page-c.md, page-e.md]. Line 2: ts 2026-04-14T18:19:26 op='fix' target_pages=[page-a.md, page-b.md, page-c.md] with an `actions` array (create_stub page-d.md, add_frontmatter_fields on page-b, add_frontmatter_block on page-c, remove_broken_link ./ghost.md). Lint precedes fix by timestamp; both carry target_page paths."
        },
        {
          "text": "Re-running /wiki-lint after the fix yields zero 'broken_links' and zero 'missing_frontmatter' violations",
          "passed": true,
          "evidence": "lint-report-after.md 'Category counts' table: broken_links = 0, missing_frontmatter = 0. Raw JSON shows summary.error = 0. Remaining issues are only warning-severity orphan_page and isolated_node rows, neither of which is in the assertion scope."
        }
      ],
      "notes": []
    },
    {
      "eval_id": 3,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 8,
        "failed": 0,
        "total": 8,
        "time_seconds": 143.6,
        "tokens": 51546,
        "tool_calls": 0,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Runs graph_ops.js with the path subcommand",
          "passed": true,
          "evidence": "outputs/graph_ops-raw-output.txt line 1-3 shows: `$ node /Users/narayan/src/doc-wiki/skills/wiki/scripts/graph_ops.js path --edges /tmp/eval-i3-path-wiki/graph/edges.jsonl --from authentication --to database-schema`. Also cited in path-answer.md lines 5-10 in the 'Command executed' block."
        },
        {
          "text": "Returned path starts at 'authentication' and ends at 'database-schema'",
          "passed": true,
          "evidence": "path-answer.md line 20: `authentication → user-model → orm-mapping → database-schema`. Raw output JSON first edge has source='authentication' (line 7) and last edge has target='database-schema' (line 23). Confirmed against fixture at /tmp/eval-i3-path-wiki/graph/edges.jsonl."
        },
        {
          "text": "Every intermediate node appears as either the source or the target of an edge in the fixture's edges.jsonl",
          "passed": true,
          "evidence": "Intermediate nodes are `user-model` and `orm-mapping`. Verified against /tmp/eval-i3-path-wiki/graph/edges.jsonl: `user-model` is the target on line 5 and source on line 3; `orm-mapping` is target on line 3 and source on line 4. path-answer.md lines 24-25 document this explicitly."
        },
        {
          "text": "Every consecutive (from, to) pair in the returned path exists as an edge in edges.jsonl (no hallucinated links)",
          "passed": true,
          "evidence": "Cross-checked each hop against the real fixture: (authentication, user-model) = line 5 (supports/INFERRED); (user-model, orm-mapping) = line 3 (supports/EXTRACTED); (orm-mapping, database-schema) = line 4 (supports/EXTRACTED). Every edge type and provenance in the answer matches the fixture exactly — no hallucinations. path-answer.md lines 31-35 provides the validation table."
        },
        {
          "text": "The returned path is shortest — no alternative chain in edges.jsonl connects the same endpoints with fewer hops",
          "passed": true,
          "evidence": "shortest-proof.md enumerates all simple paths via `allPaths(..., 20)`: Path A = 3 hops (selected) and Path B = 4 hops. Verified independently from the fixture: `authentication` has only two outgoing edges (to `session-management` and to `user-model`); neither of those reaches `database-schema` in a single additional hop (session-management only goes to user-model; user-model only to orm-mapping). So no 1-hop or 2-hop path exists, and 3 hops is provably minimal. shortest-proof.md lines 30-38 provide this no-shorter-chain proof."
        },
        {
          "text": "Edge type (supports|contradicts|extends|supersedes) is reported for each hop",
          "passed": true,
          "evidence": "path-answer.md table lines 15-18 column 'Edge type' shows supports/supports/supports for the 3 hops. Raw output JSON (graph_ops-raw-output.txt lines 11, 18, 25) has explicit `type` fields. Also per-hop in shortest-proof.md Path A table."
        },
        {
          "text": "Provenance tag (EXTRACTED|INFERRED|AMBIGUOUS) is reported for each edge",
          "passed": true,
          "evidence": "path-answer.md column 'Provenance' shows INFERRED, EXTRACTED, EXTRACTED for the three hops (lines 16-18) — matching fixture exactly. Raw JSON fields at lines 12, 19, 26 of graph_ops-raw-output.txt confirm this. shortest-proof.md Path A row (lines 11-13) also lists per-hop provenance."
        },
        {
          "text": "A no-path query (unreachable or unknown target) returns an empty result with a human-readable explanation — not a stack trace or bare []",
          "passed": true,
          "evidence": "no-path-test.md documents two cases. Case 1 (database-schema → authentication) returns `[]` raw but is wrapped in a readable explanation citing the DAG direction and offering concrete suggestions (lines 18-31). Case 2 (unknown target 'nonexistent-concept') lists known concepts and suggests /wiki-ingest and /wiki-stats (lines 46-61). 'Failure-mode notes' (lines 63-66) confirms neither invocation threw a stack trace and that exit code was 0."
        }
      ],
      "notes": []
    },
    {
      "eval_id": 4,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 6,
        "failed": 0,
        "total": 6,
        "time_seconds": 209.1,
        "tokens": 52041,
        "tool_calls": 0,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Detects Java/Spring Boot by parsing pom.xml or build.gradle — wiki.config.yaml lists framework='spring-boot' AND a framework version string that matches the fixture (e.g., '3.2.0' when pom.xml pins that version)",
          "passed": true,
          "evidence": "wiki.config.yaml lines 8-10 set language=java, build_tool=maven, framework=spring-boot; line 10 sets framework_version: 3.2.0. detection-trace.md Phase 1 cites pom.xml lines 8-13 including <version>3.2.0</version> on line 11 of the fixture pom."
        },
        {
          "text": "Detects JPA by scanning .java files — ecosystem.orm.profiles includes 'jpa' AND the detection trace lists at least one concrete @Entity class name found (e.g., 'User')",
          "passed": true,
          "evidence": "wiki.config.yaml lines 41-48 show orm.enabled: true, profiles: [jpa], detected_entities: [User, Order]. detection-trace.md Phase 2 names concrete @Entity classes 'User' (User.java line 8) and 'Order' (Order.java line 7), with @Table(name=\"users\") and @Table(name=\"orders\") annotations cited."
        },
        {
          "text": "Detects PostgreSQL from application.properties JDBC URL — wiki.config.yaml's database block sets driver='postgresql' AND host+port+database match the URL (e.g., localhost:5432/mydb when the fixture contains jdbc:postgresql://localhost:5432/mydb)",
          "passed": true,
          "evidence": "wiki.config.yaml lines 19-30: driver: postgresql, host: localhost, port: 5432, database: mydb, jdbc_url: jdbc:postgresql://localhost:5432/mydb. detection-trace.md Phase 3 shows the parse table mapping each component back to the source property spring.datasource.url=jdbc:postgresql://localhost:5432/mydb."
        },
        {
          "text": "Credentials are referenced via secret names (e.g., user_secret: WIKI_DB_DEV_USER) — plaintext passwords anywhere in wiki.config.yaml is a failure",
          "passed": true,
          "evidence": "Grep for 'password' in wiki.config.yaml returned only line 28 'password_secret: WIKI_DB_DEV_PASSWORD' (uppercase secret-store name under WIKI_ prefix) and line 32 'password_ref: ${DB_PASSWORD}' (env-var placeholder token preserved from application.properties). Grep for 'secret' returned lines 27-28, both WIKI_DB_DEV_* secret-store names. No literal password values appear anywhere in the file. credentials-check.md documents the same three grep sweeps with the same classification."
        },
        {
          "text": "Policy stanza (block_ddl, block_privilege, dml_mode, audit.enabled) is populated in the database section",
          "passed": true,
          "evidence": "wiki.config.yaml lines 33-40 under ecosystem.database: policy.block_ddl: true, block_privilege: true, dml_mode: present_only, escalate_unbounded_reads: true; audit.enabled: true with path ~/.wiki/db_audit.jsonl."
        },
        {
          "text": "Wiki scaffold (wiki/, raw/, graph/, audit/, log/, outputs/) exists at the configured wiki_root",
          "passed": true,
          "evidence": "Directory check of outputs/scaffold/ confirms all six directories present: wiki, raw, graph, audit, log, outputs. scaffold-tree.txt lists them plus subdirs (audit/open, audit/resolved, log/daily, outputs/queries, outputs/reports, wiki/claims, wiki/synthesis, wiki/templates) and the init event in events.jsonl records all created_dirs."
        }
      ],
      "notes": []
    },
    {
      "eval_id": 5,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 6,
        "failed": 0,
        "total": 6,
        "time_seconds": 265.6,
        "tokens": 61956,
        "tool_calls": 0,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Detects Python/Django — wiki.config.yaml lists language='python' and framework='django' with at least one evidence file cited in detection-trace (requirements.txt, pyproject.toml, or manage.py)",
          "passed": true,
          "evidence": "wiki.config.yaml lines 6-7: `language: python`, `framework: django`. detection-trace.md Phase 1 cites marker files `requirements.txt` (Django>=4.2, djongo, pymongo verbatim), `manage.py`, and `myapp/settings.py`."
        },
        {
          "text": "Detects Django ORM — ecosystem.orm.profiles includes 'django' AND the detection trace names at least one extracted model class with its db_table mapping (e.g., Article → articles)",
          "passed": true,
          "evidence": "wiki.config.yaml lines 38-41 set `ecosystem.orm.enabled: true` and `profiles: [django]`. detection-trace.md Phase 2 extracts `class Article(models.Model)` with `db_table = \"articles\"`, giving the exact Article → articles mapping the eval requires, plus columns title/body."
        },
        {
          "text": "Detects MongoDB — wiki.config.yaml's database block sets driver='mongodb' (via djongo or direct) with connection info parsed from settings.py (e.g., mongodb://localhost:27017)",
          "passed": true,
          "evidence": "wiki.config.yaml lines 17-29: `database.enabled: true`, `driver: mongodb`, environments.dev with host localhost, port 27017, database mydb, connection_uri `mongodb://localhost:27017`, source `myapp/settings.py (DATABASES[default] via djongo)`. detection-trace.md Phase 3 shows the parsed DATABASES stanza."
        },
        {
          "text": "Policy stanza (block_ddl, block_privilege, dml_mode, audit.enabled) is populated for MongoDB",
          "passed": true,
          "evidence": "wiki.config.yaml lines 30-37: `policy.block_ddl: true`, `block_privilege: true`, `dml_mode: present_only`, `escalate_unbounded_reads: true`, `audit.enabled: true`, path ~/.wiki/db_audit.jsonl. Mirrored and annotated in detection-trace.md Phase 3."
        },
        {
          "text": "Wiki scaffold (wiki/, raw/, graph/, audit/, log/, outputs/) exists at the configured wiki_root",
          "passed": true,
          "evidence": "wiki-scaffold-tree.txt shows all six required dirs at root /tmp/eval-i3-onboard-django: wiki/ (with claims, synthesis, templates), raw/, graph/, audit/ (open, resolved), log/ (daily), outputs/ (queries, reports). events.jsonl init event lists all 12 created_dirs."
        },
        {
          "text": "Seed files (wiki/index.md, graph/edges.jsonl, log/events.jsonl) exist — not just empty directories",
          "passed": true,
          "evidence": "seed-files-check.md confirms all three as regular files: wiki/index.md (27 bytes, '# Index\\n\\nWiki entry point.'), graph/edges.jsonl (0 bytes but a real file via touchFile), log/events.jsonl (769 bytes, 2 events). events.jsonl init event's created_files array explicitly lists all three."
        }
      ],
      "notes": []
    },
    {
      "eval_id": 1,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 6,
        "failed": 0,
        "total": 6,
        "time_seconds": 330.0,
        "tokens": 73683,
        "tool_calls": null,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Uses schema introspection (PRAGMA table_info, information_schema, or the driver's classifyOperation READ path) rather than an ad-hoc SELECT *",
          "passed": true,
          "evidence": "schema-result.md line 3: 'Source: `sqlite3` PRAGMA table_info + `sqlite_master` (read-only introspection) via `SQLiteDriver.getSchema()` — the driver's `classifyOperation` READ path.' policy-decision.json reason field corroborates: 'Schema introspection via SQLiteDriver.getSchema() (PRAGMA table_info + sqlite_master). Read-path dispatch through driver.classifyOperation → OperationType.READ.' command.txt invokes action 'schema', not a SELECT. audit.jsonl contains zero 'query' events, confirming no ad-hoc SELECT was routed through executeQuery."
        },
        {
          "text": "Result includes all four columns of the fixture (id, email, name, created_at) each paired with the expected type (INTEGER, TEXT, TEXT, TIMESTAMP) — missing or misnamed columns fail",
          "passed": true,
          "evidence": "stdout.txt lists exactly 4 columns: id/INTEGER, email/TEXT, name/TEXT, created_at/TIMESTAMP. schema-result.md table rows 1–4 mirror this: (`id`, `INTEGER`, ...), (`email`, `TEXT`, ...), (`name`, `TEXT`, ...), (`created_at`, `TIMESTAMP`, ...). Names and types match the fixture exactly."
        },
        {
          "text": "id is identified as the primary key AND email is identified as NOT NULL — a generic placeholder schema that lists columns without constraints fails",
          "passed": true,
          "evidence": "stdout.txt: id has 'is_primary_key': true; email has 'nullable': false. schema-result.md row 1 marks id pk=true; row 2 marks email nullable=false. Mermaid block also annotates 'INTEGER id PK'. Constraints are concrete, not placeholders."
        },
        {
          "text": "audit.jsonl (or the in-process audit log) contains only read-side events (connect, schema_inspect/read query, disconnect) — presence of any event whose operation is 'write', 'ddl', 'privilege', or marked executed=true for an INSERT/UPDATE/DELETE/DROP/CREATE fails this assertion",
          "passed": true,
          "evidence": "audit.jsonl contains a single event: {\"event_type\": \"pool_created\", ..., \"details\": {\"env\": \"dev\", \"driver\": \"sqlite\"}}. This is a connection-lifecycle event (read-side). audit-shape-check.md confirms via jq: 0 write/ddl/privilege/executed-mutation events, 0 policy_deny events, 0 query events. The assertion's fail conditions (write/ddl/privilege/executed-true mutations) are all absent, so the assertion passes on its literal terms. Note: the log is thin — it does not contain discrete schema_inspect or disconnect events; see eval_feedback below."
        },
        {
          "text": "policy-decision.json records final_decision='allow' with env='dev' and approval_mode='auto'",
          "passed": true,
          "evidence": "policy-decision.json lines 2–4: \"final_decision\": \"allow\", \"env\": \"dev\", \"approval_mode\": \"auto\". All three fields match exactly."
        },
        {
          "text": "A Mermaid ER diagram for the users table is emitted (the agent contract requires mermaid output on schema queries)",
          "passed": true,
          "evidence": "stdout.txt lines 40–44 include a `mermaid` key with type 'erDiagram' and code 'erDiagram\\n    users {\\n        INTEGER id PK\\n        TEXT email\\n        TEXT name\\n        TIMESTAMP created_at\\n    }'. schema-result.md lines 31–39 render the same fenced ```mermaid erDiagram``` block. Contract is satisfied."
        }
      ],
      "notes": []
    },
    {
      "eval_id": 2,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 5,
        "failed": 0,
        "total": 5,
        "time_seconds": 218.0,
        "tokens": 63401,
        "tool_calls": 0,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Row count in the users table is 0 before AND after the run — any increment is an execution leak and a critical failure",
          "passed": true,
          "evidence": "row-count-before.txt contains '0' and row-count-after.txt contains '0'. No execution leak — the INSERT was never routed to the sqlite driver."
        },
        {
          "text": "The returned SQL is an INSERT INTO users statement containing both 'Test' AND 'test@example.com' verbatim (semantic equivalence to the request)",
          "passed": true,
          "evidence": "stdout.txt formatted_sql: \"INSERT INTO users (name, email) VALUES ('Test', 'test@example.com')\" — target=users, columns=(name, email), values=('Test', 'test@example.com') all present verbatim. Confirmed in content-equivalence-check.md."
        },
        {
          "text": "Policy decision is PRESENT_ONLY and appears in policy-decision.md alongside the rule that matched (DML classifier)",
          "passed": true,
          "evidence": "policy-decision.md records policy_gate=PRESENT_ONLY, final_decision=present_only, operation_type=DML (INSERT), and cites the DML classifier verbatim from agents/lib/wiki_db/policy.ts including the _DML_KEYWORDS set ({INSERT, UPDATE, DELETE, REPLACE, MERGE, UPSERT}) and the PRESENT_ONLY branch of Policy.checkQuery."
        },
        {
          "text": "Output clearly labels the SQL as not executed (e.g., title 'NOT EXECUTED', status='present_only', executed=false)",
          "passed": true,
          "evidence": "presented-sql.md title: 'NOT EXECUTED — Presented SQL (DML PRESENT_ONLY)'; status: 'present_only'; executed: false. stdout.txt status field is literally \"present_only\" with reason \"DML statements are displayed but not executed\"."
        },
        {
          "text": "No audit event with a write/executed shape is emitted for this query — only a policy evaluation entry is permitted",
          "passed": true,
          "evidence": "Verified directly: `ls -la /tmp/eval-i3-db-po/audit.jsonl` returns 'No such file or directory'. audit.enabled=true in wiki.config.yaml but no file was ever created because the PRESENT_ONLY branch short-circuits before driver.execute and the present_only path does not call _emitDeny (which is the sole write path for policy_deny events)."
        }
      ],
      "notes": []
    },
    {
      "eval_id": 3,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 6,
        "failed": 0,
        "total": 6,
        "time_seconds": 288.6,
        "tokens": 66484,
        "tool_calls": 0,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Policy decision is DENY (final_decision='deny' in policy-decision.md with the DDL rule cited)",
          "passed": true,
          "evidence": "policy-decision.md states '**final_decision**: `deny`' and cites the DDL rule verbatim: 'DDL statements are never allowed (hardcoded deny for OperationType.DDL, policy.ts:211-216)'. operation_type is classified as 'ddl' via classifySqlKeywords because 'DROP' matches _DDL_KEYWORDS. Verified against policy.ts:213 which returns reason='DDL statements are never allowed' for OperationType.DDL."
        },
        {
          "text": "The response message explicitly states DDL is blocked and names the environment (production)",
          "passed": true,
          "evidence": "deny-message.md contains the literal sentence 'The query `DROP TABLE users` was rejected by the policy gate on environment **production** because `DROP` is classified as a DDL operation' and echoes the exact reason string 'DDL statements are never allowed'. The 'Assertion coverage' section explicitly calls out both requirements."
        },
        {
          "text": "End-to-end invocation of db_query.js --env production --sql 'DROP TABLE users' completes in under 500ms of wall time - slower than this and the CLI would have attempted DNS/TCP to the (deliberately unreachable) production host before the policy check",
          "passed": true,
          "evidence": "timing.md reports /usr/bin/time -p measurement 'real 0.05 / user 0.04 / sys 0.01' = 50ms wall, 10x under the 500ms budget. Reproduced in-grader: running the same command produced real 0.05, user 0.03, sys 0.00, exit=1. command.txt confirms the invocation is the full-path node invocation of db_query.js with --env production --config /tmp/eval-i3-db-deny/wiki.config.yaml --sql 'DROP TABLE users' - a genuine end-to-end CLI call, not a library-level shortcut."
        },
        {
          "text": "No audit event of type 'connect', 'pool_created', or 'query_executed' is written for env=production - a clean run contains only the policy_deny entry",
          "passed": true,
          "evidence": "audit.jsonl is 0 bytes (wc -c confirms). audit-shape-check.md documents the zero-event outcome with jq enumeration showing 0 connect / 0 pool_created / 0 query_executed. CAVEAT (transparently noted by the executor): the zero-byte outcome is vacuously true because db_query.ts does not yet call enableAudit() from config - so there is no positive 'policy_deny' entry either. Per grader instructions this caveat is recorded but the assertion passes because the timing+stderr evidence independently corroborates no driver activity."
        },
        {
          "text": "stderr is empty - a real connection attempt to a nonexistent host would produce ENOTFOUND/ECONNREFUSED; its absence corroborates that policy ran before any driver activity",
          "passed": true,
          "evidence": "stderr.txt is 0 bytes (wc -c confirms). No ENOTFOUND/ECONNREFUSED/ETIMEDOUT strings anywhere. Reproduced in-grader: the end-to-end invocation produced no stderr output. Combined with the 50ms wall time, this rules out any DNS lookup or TCP connect against the pg host."
        },
        {
          "text": "Exit code is non-zero and distinguishes policy deny from a generic error (e.g., a dedicated exit code or a parseable marker in stdout/stderr)",
          "passed": true,
          "evidence": "Reproduced exit=1 in-grader. Stdout contains the machine-parseable JSON {\"status\":\"denied\",\"reason\":\"DDL statements are never allowed\",\"execution_time_ms\":0.14} - the 'status':'denied' field (distinct from 'error' or 'ok') is the dedicated marker. db_query.ts line 162 confirms 'status=denied' is an intentional, distinct code path, and line 426/429 shows process.exit(code) / process.exit(1) is the exit path. policy-decision.md explicitly states 'Exit code `1`. status==\"denied\" is machine-parseable in stdout, distinguishing policy deny from a generic error.'"
        }
      ],
      "notes": [
        "db_query.ts does not call enableAudit() when wiki.config.yaml sets audit.enabled=true; the assertion about 'no connect/pool_created/query_executed audit events' is vacuously satisfied rather than positively proved. Timing+stderr evidence still carries the assertion.",
        "Executor saved a 0-byte audit.jsonl placeholder in outputs/ to make proof-of-absence explicit, since the CLI never created the file."
      ]
    },
    {
      "eval_id": 1,
      "configuration": "with_skill",
      "run_number": 1,
      "result": {
        "pass_rate": 1.0,
        "passed": 8,
        "failed": 0,
        "total": 8,
        "time_seconds": 173.1,
        "tokens": 38158,
        "tool_calls": 0,
        "errors": 0
      },
      "expectations": [
        {
          "text": "Both User AND Order are identified as @Entity classes — detected-entities.json lists class_name: 'User' and class_name: 'Order'",
          "passed": true,
          "evidence": "outputs/detected-entities.json: entities[0].class_name='Order' and entities[1].class_name='User'. Both @Entity classes from fixtures/jpa/ were detected."
        },
        {
          "text": "@Table(name) maps to table names 'users' and 'orders' respectively; User also carries schema='public' from @Table(schema=\"public\")",
          "passed": true,
          "evidence": "outputs/detected-entities.json shows {class_name:'Order', table_name:'orders', schema:''} and {class_name:'User', table_name:'users', schema:'public'}. Matches @Table(name=\"users\", schema=\"public\") on User.java and @Table(name=\"orders\") on Order.java."
        },
        {
          "text": "@OneToMany and @ManyToOne relationships are captured — detected-entities.json includes at least one one_to_many and one many_to_one relationship",
          "passed": true,
          "evidence": "outputs/detected-entities.json: User.relationships contains 'one_to_many' (from @OneToMany orders); Order.relationships contains 'many_to_one' (from @ManyToOne user). Both present."
        },
        {
          "text": "Every relationship in detected-entities.json has a non-empty target_entity that names another extracted class — no blank target_entity strings",
          "passed": true,
          "evidence": "F3 fix verified. The extractor output (entities-full.json, relationships-check.md) shows all 3 relationships have non-empty target_entity: Order.many_to_one->User, User.one_to_many->Order, User.many_to_many->Role. 0/3 empty. Note: the public orm_detect.js JSON contract (detected-entities.json) emits only the relationship type string — it does not serialize target_entity at all — so the assertion's literal wording ('in detected-entities.json') cannot be satisfied by the public artifact. The executor documented this by saving the extractor's full object graph to entities-full.json and citing it in relationships-check.md; the underlying F3 fix is correct."
        },
        {
          "text": "Mermaid erDiagram references real extracted tables at both endpoints — no '<table>_rel' placeholder, no phantom nodes",
          "passed": true,
          "evidence": "grep for '_rel' across outputs/database-mapping.md returns 0 matches (confirmed in mermaid-er-check.md and re-run by grader). Both edges terminate at real extracted nodes ('users', 'orders', 'role'). No phantom '<class>_rel' leakage."
        },
        {
          "text": "Mermaid erDiagram contains an edge between 'users' and 'orders' (either ||--o{ or }o--||) and uses 'users' and 'orders' as literal node names",
          "passed": true,
          "evidence": "outputs/database-mapping.md line 30: 'users ||--o{ orders : \"\"'. Both 'users' and 'orders' appear as literal ER node declarations (with attribute blocks) earlier in the block."
        },
        {
          "text": "Generated markdown has frontmatter with type='entity' and orm_profile='jpa'",
          "passed": true,
          "evidence": "outputs/database-mapping.md line 3 'type: entity' and line 6 'orm_profile: jpa'. Frontmatter also sets tags:[database,orm,jpa] and generated_by:orm-mapper."
        },
        {
          "text": "mermaid_lint.js reports no syntax issues on the generated page",
          "passed": true,
          "evidence": "outputs/mermaid-lint.txt contains '[]' (empty JSON array). Grader re-ran `node mermaid_lint.js --page outputs/database-mapping.md` and got identical '[]' output."
        }
      ],
      "notes": []
    }
  ],
  "run_summary": {
    "with_skill": {
      "pass_rate": {
        "mean": 1.0,
        "stddev": 0.0,
        "min": 1.0,
        "max": 1.0
      },
      "time_seconds": {
        "mean": 240.0786,
        "stddev": 71.7757,
        "min": 143.6,
        "max": 389.3
      },
      "tokens": {
        "mean": 57038.9286,
        "stddev": 12978.2369,
        "min": 38158,
        "max": 80777
      }
    },
    "delta": {
      "pass_rate": "+1.00",
      "time_seconds": "+240.1",
      "tokens": "+57039"
    }
  },
  "notes": [
    "Iteration-3 pass rate: 100.0% (68/68 assertions). Iteration-2 was 98.5% (67/68). The +1.5pp delta comes from F1 (init_wiki.ts now emits op=init) clearing the single failing assertion from iter-2.",
    "The three skill-code defects from iter-2 grader feedback were all fixed and verified landed:",
    "  F1 — init_wiki.ts emits op=init (eval-1 assertion #10 now passes; previously impossible to satisfy).",
    "  F2 — mermaid_gen prefers <!-- wiki-mermaid: start/end --> markers when present, replacing the EOF append (eval-11 and eval-12 tightened assertions now require marker-based injection; both pass).",
    "  F3 — JPA extractor resolves relationship target_entity from generic type args; output.ts uses real table names instead of the `<table>_rel` placeholder (eval-9 assertions assert no phantom + explicit users↔orders edge; both pass).",
    "Ten additional eval assertions were tightened from presence-based to substance-based (citation resolution, body preservation, path-is-shortest, framework_version value, JDBC URL parts, no plaintext credentials, specific column+type tuples, audit event shape, content-equivalence of presented SQL, end-to-end CLI for DENY, marker-count invariants, adversarial preservation). All pass — the skill genuinely satisfies them, not just the weaker originals.",
    "Grader critiques still surface residual gaps: (a) db_query.ts does not wire enableAudit() from wiki.config.yaml — the audit-event assertions are vacuously satisfied for DENY/PRESENT_ONLY paths. A future iteration could wire audit properly and re-harden. (b) JPA Role relationship falls back to lowercased class name because Role.java is not in the fixture — the F3 fix is correct for in-extraction targets but degrades gracefully for external ones. (c) orm-custom-profile works via a deliberate table_pattern bypass; the extractor still has first-match-wins semantics on table_pattern globally.",
    "Mean duration up from iter-2: 171s → 240s (+40%). Tighter assertions forced subagents to produce more substantive evidence (shortest-path proofs, sha256 preservation checks, end-to-end CLI traces, idempotency round-trips). Token spend roughly flat (57k mean).",
    "18 live-API agent prompts remain skipped — see skipped-evals.md. No change this iteration."
  ]
}
