@dzhechkov/keysarium 1.7.13 → 1.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.dz-manifest.json CHANGED
@@ -9,11 +9,11 @@
9
9
  },
10
10
  {
11
11
  "path": "README.md",
12
- "sha256": "399f8032d0723d371c63c4d94d351614f4a2e11865a6475ab3f1db5c02e45058"
12
+ "sha256": "a590413d707d91ebe9cf15a204bfce3988bccccb0727eca0cc1e47aaadaf2684"
13
13
  },
14
14
  {
15
15
  "path": "README.ru.md",
16
- "sha256": "7fe1563b4f7a4ebe165fc0f9ef7be040db6e6bbd4cd7a98ae753bcf942c3cd40"
16
+ "sha256": "acccc3d79ba5cbdb5d2a0b2b38328d55e8f38c3209e9b538be57917da8344ecd"
17
17
  },
18
18
  {
19
19
  "path": "bin/cli.js",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  {
23
23
  "path": "package.json",
24
- "sha256": "3ab5dc27c4025613b6d9d9c5932eff6ef31927190723f63a8fd32b68e462050b"
24
+ "sha256": "4e6cb01409f1361cedfd28baff49920073a182c976a21ffa761c994f400a4641"
25
25
  },
26
26
  {
27
27
  "path": "src/cli.js",
@@ -73,7 +73,7 @@
73
73
  },
74
74
  {
75
75
  "path": "templates/.claude/commands/analyst-manual.md",
76
- "sha256": "9c84b795424197c9fb4ac6be8c444808a8a04cf829434c9b7c5c581a61b5e966"
76
+ "sha256": "924206fde1d36f98b31c2d4495edc506499d58e89464a75fb872774d2426638c"
77
77
  },
78
78
  {
79
79
  "path": "templates/.claude/commands/architecture-phase.md",
@@ -277,7 +277,7 @@
277
277
  },
278
278
  {
279
279
  "path": "templates/.claude/skills/analyst-manual-full/SKILL.md",
280
- "sha256": "4733b487b8a2772ada4108561429611308e7a785c43d4dab0de8c9112637f3d0"
280
+ "sha256": "ebcb4decc419af099b94cf66fa588ca5cd837e3dfb0587170206093cb786ac72"
281
281
  },
282
282
  {
283
283
  "path": "templates/.claude/skills/edu-site-generator/SKILL.md",
@@ -385,15 +385,15 @@
385
385
  },
386
386
  {
387
387
  "path": "templates/.claude/skills/feature-adr/modules/06-implementation-plan.md",
388
- "sha256": "d7a2a4e56b24451da234b1ac4ef440ba9b0370617e1c0cde8ad5f3d575a53209"
388
+ "sha256": "4eae99dfc266f9c32be3fd302b6a8316924461db8ae5038a169d7a0ec6398e89"
389
389
  },
390
390
  {
391
391
  "path": "templates/.claude/skills/feature-adr/modules/07-code.md",
392
- "sha256": "d5c03890b2b3a34277c73a14d3d7c97741dec1d6c382e87ee4cc6a5a52d938d7"
392
+ "sha256": "7cd05898db005ad19fbaeb25af3d958d47946f5db195e6969009fd45953e3e30"
393
393
  },
394
394
  {
395
395
  "path": "templates/.claude/skills/feature-adr/modules/08-qe.md",
396
- "sha256": "e2fb5fb869005b05d13fcc52da7ded281bb6758250a1cdf695dace286029d791"
396
+ "sha256": "4c80cbbae3c4acfb61e4cd0dfbf97e873ef7bded9d41ecaf84959e6fe5e0640f"
397
397
  },
398
398
  {
399
399
  "path": "templates/.claude/skills/feature-adr/modules/09-fleet-qe.md",
@@ -493,16 +493,24 @@
493
493
  },
494
494
  {
495
495
  "path": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
496
- "sha256": "8b93949ce4f671d932c5389050db3a3e2750efcbed69681d69f392ccf4d2a168"
496
+ "sha256": "1c8f8e6ffad01ebdb1bcf6fce6cfb6baae6fe40a9ce377b3894f8addc06c8180"
497
497
  },
498
498
  {
499
499
  "path": "templates/.claude/skills/feature-adr/scripts/check-review-convergence.mjs",
500
- "sha256": "6fdacc0ee6c1d2b00029da8d300a574c051895ee98a9c88c9509a39b9bdccc71"
500
+ "sha256": "91fe091ed7ab8f38340b8ac0fd3194c90b357cb9bab381a86594ac96600ced97"
501
+ },
502
+ {
503
+ "path": "templates/.claude/skills/feature-adr/scripts/expected-target-grammar.mjs",
504
+ "sha256": "e763efd3af9c7ba71f758f6d6c349b21f2a41ded0420fc165269eab82b6433fb"
501
505
  },
502
506
  {
503
507
  "path": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
504
508
  "sha256": "82c3c48c25f400f3a64d1caba3d47357f3db87460f0746f88706f901eb6ff689"
505
509
  },
510
+ {
511
+ "path": "templates/.claude/skills/feature-adr/scripts/native-post-ci-evidence.mjs",
512
+ "sha256": "b19b5491cb42f1e9f2fdefd642e7938640d68fd8bdff991f66fdcebef61f55ae"
513
+ },
506
514
  {
507
515
  "path": "templates/.claude/skills/frontend-design/LICENSE.txt",
508
516
  "sha256": "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594"
@@ -1005,5 +1013,5 @@
1005
1013
  }
1006
1014
  ]
1007
1015
  },
1008
- "signature": "IuyuSO5ojzp0YmewWW4Kmtn854zfx7p7vZmpVXBpzSnV7G24mTKKMJk2QAv0GjtLp9kfiiWwy0WkecuW7nPdCQ=="
1016
+ "signature": "eyp74YajdZXj/DTT1fEaRgwmHs+b6ohWmMy8kCssElJV7BriGhzfBo+R3OULGy2kiWM46j03kdBadjTXM7jbAg=="
1009
1017
  }
package/README.md CHANGED
@@ -5,7 +5,7 @@ The bundled Feature ADR projection supports installed-CLI native QE history opt-
5
5
  If bytes change before first evaluation, native `evaluate` retains valid old-snapshot receipt origins and conditions while reporting `not-established/native-repair-required`. Follow with explicit `begin-repair` and fresh independent verification of the new snapshot. Recovery preserves authority and the three-cycle ceiling; old evidence never closes changed bytes.
6
6
 
7
7
 
8
- Current package version: `1.7.13`. <!-- dz:version -->
8
+ Current package version: `1.7.15`. <!-- dz:version -->
9
9
 
10
10
  Site: https://aicoding.space · Source: https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/keysarium
11
11
 
@@ -13,6 +13,12 @@ Site: https://aicoding.space · Source: https://github.com/djd1m/dz-harness/tree
13
13
 
14
14
  Full 7-phase pipeline for AI case studies, hackathons, and casariums. Provides skills, commands, rules, shards, agent templates, and brain portability for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
15
15
 
16
+ The bundled `analyst-manual-full` research checkpoint uses `moderate` for ordinary web and health
17
+ sources. It offers `strict` or `paranoid` only when pinned issuer keys and signed delivery are
18
+ established for the selected source class. If a requested mode lacks either capability, research
19
+ stops with the missing condition instead of claiming cryptographic verification. Its reports
20
+ distinguish cited sources, issuer verification, and citation-chain integrity.
21
+
16
22
  Bundled feature-adr usage guidance records observed stage metadata/source IDs through existing
17
23
  writers and reads explicit Wf/FA project evidence. Unknown dimensions and full prices stay null;
18
24
  independent verification, family estimates and billing absence remain visible.
@@ -562,3 +568,21 @@ terminal `RC_EXIT:0` witness; failure refuses coding/cache access. Admission pre
562
568
  and existing `.fa-state/base-ref` (HEAD only when absent); actual coder initialization never resets
563
569
  an existing base. Plain uses the same executable helper and mandatory instructions, without an
564
570
  automatic dispatcher. Admission observes current bytes; final QE remains the freshness check.
571
+
572
+ ### Literal implementation-plan targets
573
+
574
+ Feature-ADR K2 completeness, native admission and core plan authority share a raw-text trailing
575
+ `EXPECTED_CODE_TARGETS:` grammar: one uppercase column-zero marker with optional ASCII padding,
576
+ LF-only declaration, nonempty `- ` path rows and trim()-empty blanks through EOF. Suffix headings,
577
+ prose, alternate bullets, CR/CRLF or duplicate raw markers refuse the whole block; markers inside
578
+ examples/comments also count. Payloads stay literal, including backticks and whitespace. Existing
579
+ path authority and native I/O/history bounds still apply. Core establishes no valid subset from
580
+ malformed or rejected/rewritten plan paths. Explicit args retain their separate normalization and
581
+ precedence; coder text remains diagnostic. The standalone grammar helper ships beside the skill's
582
+ native checker and K2 script. No Markdown authenticity is inferred.
583
+
584
+ Native QE also supports one explicit `begin-post-ci-correction` after the current witnessed fourth cycle is CLOSED. Supply the original `--reviewers`, strict `--owner-approval` declaration (`native-qe-post-ci-owner-approval-1`, including the exact descriptor SHA256), and `--correction-evidence <repo-relative-json-file>`. Core and the installed helper require post-CI capability 2; history and ordinary continuation APIs remain 1. Default ceiling three and the ordinary continuation remain unchanged; the separate fifth target is pinned and no sixth cycle is available.
585
+
586
+ The canonical local descriptor binds failed CI bytes, one existing test fixture patch, enumerated accepted Git integration, unchanged dependencies, genuine color/neutral full controls and the complete successful mandatory CI identity set. Logs stream within separate 16 MiB/file, four-log/64 MiB bounds; existing source and transaction limits remain unchanged. This is local measured evidence, not remote authentication. Required proof and checkpoints remain obligations. The correction QA namespace must contain exactly its descriptor, referenced proofs and digest-bound `excludedEvidence` supplements, including tracked, untracked and ignored files. Extra entries or changed/missing listed proof block availability; keep inline consent and evolving native/review outputs outside this immutable namespace. Complete fifth receipts may list reviewers in any order; durable closure preserves the original roster order. A mutating evaluate that records loss or replacement invalidates earlier per-reviewer proof verifications; restored bytes require fresh originating receipts bound to the durable observation anchor. Reads and refused prepare do not remember transient loss. Use accepted signed packed local packages before authoritative admission; this change does not publish or activate packages globally.
587
+
588
+ Schema 2 (`native-qe-post-ci-evidence-2`) adds exact `scope.importedArtifacts` rows (`path`, absent `before`, SHA256 `digest`, byte `size`) for foreign feature history imported by a normal two-parent merge. Schema 1 remains supported with its original closed shape. Selected foreign roots must be absent from the failed/base/preimport trees and have identical complete membership and bytes in accepted, integrated, qualified and current views. Ignored or untracked extras, active-feature/authority paths, source/proof overlap, links and file/directory aliases refuse admission. Imported support shares the existing 2 MiB/file and 16 MiB source aggregate; schema 2 jointly bounds immutable proof/support to 256 files and 256 derived directories. Source manifests remain limited to 256 entries. Support is reobserved by core; recorded loss requires fresh bound originating reviews after restoration.
package/README.ru.md CHANGED
@@ -49,7 +49,7 @@ DISCOVERY EXPLORE RESEARCH CJM ПРОТОТИП
49
49
  |------|---------|-------|-----------|----------|
50
50
  | **0** | `/discovery` | reverse-engineering-unicorn | JTBD + конкуренты + ROI + бизнес-кейс | `00_product_discovery.md` |
51
51
  | **1** | `/explore-case` | explore | Глубокое понимание кейса, 5 Whys, критерии успеха | `01_case_brief.md` |
52
- | **2** | `/research` | goap-research-ed25519 | PARANOID research: аналоги, технологии, регуляторика (3 параллельных агента) | `02_research_findings.md` |
52
+ | **2** | `/research` | goap-research-ed25519 | Исследование аналогов, технологий и регуляторики с явным статусом источников (3 параллельных агента) | `02_research_findings.md` |
53
53
  | **2.5** ⭐ | `/cjm-prototype` | reverse-eng + frontend-design | 3 варианта CJM → кликабельный React прототип → выбор | `prototype/cjm-prototype.jsx` |
54
54
  | **3** | `/solve` | problem-solver-enhanced | Стратегия решения: SCQA, Process Design, AI Pipeline, HITL | `03_solution_strategy.md` |
55
55
  | **4** | `/architecture-phase` | (встроенный) | C4 диаграммы, компоненты, безопасность, MVP scope | `04_architecture.md` |
@@ -59,7 +59,7 @@ DISCOVERY EXPLORE RESEARCH CJM ПРОТОТИП
59
59
  - Phase 2.5 (CJM) — **ОБЯЗАТЕЛЬНА, нельзя пропустить**
60
60
  - `08_executive_summary.md` — **ОБЯЗАТЕЛЕН**
61
61
  - Checkpoint после каждой фазы — ждёт `"ок"` от пользователя
62
- - Research в PARANOID mode — ноль непроверенных утверждений
62
+ - Research помечает неподтверждённые утверждения; `strict` и `paranoid` требуют подтверждённой возможности проверки подписей источника
63
63
  - `{CHOSEN_CJM}` передаётся из Phase 2.5 в Phase 3-5
64
64
 
65
65
  ---
@@ -71,7 +71,7 @@ DISCOVERY EXPLORE RESEARCH CJM ПРОТОТИП
71
71
  ```
72
72
  reverse-engineering-unicorn (оркестратор pipeline)
73
73
  ├── explore (Socratic кларификация)
74
- ├── goap-research-ed25519 (GOAP + Ed25519 верификация)
74
+ ├── goap-research-ed25519 (GOAP + статус источников; проверка подписи при наличии условий)
75
75
  ├── problem-solver-enhanced (TRIZ + Game Theory + 5 Whys)
76
76
  ├── frontend-design (UI для CJM прототипа)
77
77
  └── presentation-storyteller (слайды + speaker script)
@@ -91,18 +91,18 @@ reverse-engineering-unicorn (оркестратор pipeline)
91
91
 
92
92
  **Когда вызывается:** Phase 1, а также как зависимость в reverse-engineering-unicorn, presentation-storyteller, analyst-manual, feature-adr, ai-factory-mapper.
93
93
 
94
- ### 2. goap-research-ed25519 — верифицированный research
94
+ ### 2. goap-research-ed25519 — исследование с проверяемыми ограничениями
95
95
 
96
- **Что делает:** GOAP (Goal-Oriented Action Planning) + OODA loop + Ed25519 криптографические подписи для антигаллюцинации.
96
+ **Что делает:** GOAP (Goal-Oriented Action Planning) + OODA loop; отделяет ссылки на источники от криптографического подтверждения их происхождения.
97
97
 
98
98
  **Протокол:**
99
99
  1. Оценивает текущее состояние (что знаем / не знаем)
100
100
  2. Строит план исследования через A* алгоритм с учётом стоимости верификации
101
- 3. Выполняет OODA цикл (Observe → Orient → Decide → Act) с checkpoint'ами подписей
102
- 4. Каждый факт подписывается Ed25519 → цепочка доверия (citation chain)
103
- 5. Генерирует подписанный research report
101
+ 3. Выполняет OODA цикл (Observe → Orient → Decide → Act) с контрольными точками
102
+ 4. Для каждого вывода указывает источник и степень подтверждения; подпись издателя проверяется только при наличии подписанной выдачи и закреплённого ключа
103
+ 5. Формирует отчёт с отдельным статусом источников, подписей и цепочки цитирования
104
104
 
105
- **3 уровня строгости:** moderate (0.85), strict (0.95), paranoid (0.99)
105
+ **3 уровня строгости:** `moderate` (0.85) доступен для обычных веб-источников. `strict` (0.95) и `paranoid` (0.99) требуют реальных ключей издателей и подписанной выдачи выбранного класса источников; без них план возвращает `GOAL_UNREACHABLE`.
106
106
 
107
107
  **Когда вызывается:** Phase 2 (основной), а также все DEEP/VERIFIED режимы в других скиллах.
108
108
 
@@ -142,7 +142,7 @@ reverse-engineering-unicorn (оркестратор pipeline)
142
142
 
143
143
  **Протокол:**
144
144
  1. Кларификация (explore) → цель, аудитория, ограничения
145
- 2. Research (goap-research-ed25519) → верифицированные данные и источники
145
+ 2. Research (goap-research-ed25519) → данные с источниками и явными ограничениями проверки
146
146
  3. Структура (storytelling arc + outline с тезисами)
147
147
  4. Полная презентация в Markdown со ссылками на источники
148
148
  5. Speaker script «Как рассказывать?» — связная история для каждого слайда
@@ -180,8 +180,8 @@ reverse-engineering-unicorn (оркестратор pipeline)
180
180
  **Что делает:** Композитный оркестратор: explore → goap-research → problem-solver с ручными checkpoint'ами.
181
181
 
182
182
  **3 фазы с подтверждением:**
183
- 1. Explore → Task Brief → **CHECKPOINT** (выбор verification mode: moderate/strict/paranoid)
184
- 2. Verified Research (Ed25519) → **CHECKPOINT**
183
+ 1. Explore → Task Brief → **CHECKPOINT** (`moderate` по умолчанию; `strict`/`paranoid` только после проверки возможностей источников)
184
+ 2. Research с явным статусом источников и подписей → **CHECKPOINT**; при `GOAL_UNREACHABLE` переход к Solve не выполняется
185
185
  3. Solve (9-module framework) → **CHECKPOINT**
186
186
  4. Синтез: 4 документа × 2 формата (md + docx)
187
187
 
@@ -215,7 +215,7 @@ reverse-engineering-unicorn (оркестратор pipeline)
215
215
  |-------|---------|--------|-------|
216
216
  | 🟢 **QUICK** | Статические запросы | Шаблоны | ~70 мин |
217
217
  | 🔵 **DEEP** | GOAP A* + OODA | + Game Theory, TRIZ, CJM прототип, BS-check | ~140 мин |
218
- | 🟣 **VERIFIED** | GOAP + Ed25519 | + Криптоподписи, audit trail, trusted issuers | ~170 мин |
218
+ | 🟣 **VERIFIED** | GOAP + проверка происхождения при наличии подписанной выдачи и ключей издателей | Audit trail и явные ограничения проверки; подпись не доказывает истинность факта | ~170 мин |
219
219
 
220
220
  ### 7 модулей
221
221
 
@@ -226,7 +226,7 @@ reverse-engineering-unicorn (оркестратор pipeline)
226
226
  #### Module 1: INTELLIGENCE → Verified Fact Sheet
227
227
  - Год основания, штаб-квартира, команда, финансирование
228
228
  - Ключевые метрики: выручка, MAU, NPS
229
- - Каждый факт верифицирован (DEEP → GOAP, VERIFIED → Ed25519)
229
+ - Для каждого факта указываются источник и уровень уверенности; наличие подписи издателя отражается отдельно
230
230
  - **Артефакт:** Fact Sheet с confidence scores
231
231
 
232
232
  #### Module 2: PRODUCT & CUSTOMERS → JTBD + Segments + Voice of Customer
@@ -313,7 +313,7 @@ Module 2.5: Agent 1 (opus) → Variant A
313
313
  | `/casarium --ai-factory [текст]` | Все | + маппинг на Cloud.ru AI Factory |
314
314
  | `/discovery` | 0 | Product Discovery |
315
315
  | `/explore-case` | 1 | Глубокое понимание кейса |
316
- | `/research` | 2 | PARANOID research (3 агента) |
316
+ | `/research` | 2 | Исследование с проверкой источников (3 агента); криптографический режим зависит от возможностей источника |
317
317
  | `/cjm-prototype` | 2.5 | CJM → React прототип |
318
318
  | `/solve` | 3 | Стратегия решения |
319
319
  | `/architecture-phase` | 4 | Техническая архитектура |
@@ -355,7 +355,7 @@ Module 2.5: Agent 1 (opus) → Variant A
355
355
 
356
356
  | Правило | Назначение |
357
357
  |---------|-----------|
358
- | `research-quality` | PARANOID mode: каждый факт должен иметь источник, confidence ≥ 0.99 |
358
+ | `research-quality` | Политика источников с названием PARANOID: для каждого факта требуется источник и confidence ≥ 0.99; это не подтверждает подпись издателя |
359
359
  | `checkpoint-protocol` | Формат checkpoint'ов, promise tags, запрет авто-продвижения |
360
360
  | `agent-swarm` | Когда и как запускать параллельных агентов, модели, изоляция |
361
361
  | `domain-specific` | Авто-детекция домена → доменные правила (Banking, Retail, Enterprise, Healthcare) |
@@ -396,6 +396,9 @@ Module 2.5: Agent 1 (opus) → Variant A
396
396
  | `phase-ai-factory` | AI Factory (все фазы) | Только при `--ai-factory` |
397
397
  | `feature-adr` | Feature Development | Независим от casarium |
398
398
 
399
+ Имя checkpoint `RESEARCH_PARANOID_PASSED` сохраняется в существующем pipeline. Оно обозначает
400
+ прохождение его исследовательского этапа, а не доказанную Ed25519-подпись каждого источника.
401
+
399
402
  ---
400
403
 
401
404
  ## Система самообучения
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/keysarium",
3
- "version": "1.7.13",
3
+ "version": "1.7.15",
4
4
  "scripts": {},
5
5
  "description": "AI Case Research Toolkit for Claude Code — full 7-phase pipeline with feature-adr, dream cycles, background workers, multi-platform support, reward learning, and Cloud.ru AI Factory mapping (--ai-factory flag)",
6
6
  "main": "src/cli.js",
package/sbom.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "399f8032d0723d371c63c4d94d351614f4a2e11865a6475ab3f1db5c02e45058"
28
+ "content": "a590413d707d91ebe9cf15a204bfce3988bccccb0727eca0cc1e47aaadaf2684"
29
29
  }
30
30
  ]
31
31
  },
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "7fe1563b4f7a4ebe165fc0f9ef7be040db6e6bbd4cd7a98ae753bcf942c3cd40"
38
+ "content": "acccc3d79ba5cbdb5d2a0b2b38328d55e8f38c3209e9b538be57917da8344ecd"
39
39
  }
40
40
  ]
41
41
  },
@@ -59,7 +59,7 @@
59
59
  },
60
60
  {
61
61
  "name": "dz:canonical-json-sha256-v2",
62
- "value": "3ab5dc27c4025613b6d9d9c5932eff6ef31927190723f63a8fd32b68e462050b"
62
+ "value": "4e6cb01409f1361cedfd28baff49920073a182c976a21ffa761c994f400a4641"
63
63
  }
64
64
  ]
65
65
  },
@@ -189,7 +189,7 @@
189
189
  "hashes": [
190
190
  {
191
191
  "alg": "SHA-256",
192
- "content": "9c84b795424197c9fb4ac6be8c444808a8a04cf829434c9b7c5c581a61b5e966"
192
+ "content": "924206fde1d36f98b31c2d4495edc506499d58e89464a75fb872774d2426638c"
193
193
  }
194
194
  ]
195
195
  },
@@ -699,7 +699,7 @@
699
699
  "hashes": [
700
700
  {
701
701
  "alg": "SHA-256",
702
- "content": "4733b487b8a2772ada4108561429611308e7a785c43d4dab0de8c9112637f3d0"
702
+ "content": "ebcb4decc419af099b94cf66fa588ca5cd837e3dfb0587170206093cb786ac72"
703
703
  }
704
704
  ]
705
705
  },
@@ -969,7 +969,7 @@
969
969
  "hashes": [
970
970
  {
971
971
  "alg": "SHA-256",
972
- "content": "d7a2a4e56b24451da234b1ac4ef440ba9b0370617e1c0cde8ad5f3d575a53209"
972
+ "content": "4eae99dfc266f9c32be3fd302b6a8316924461db8ae5038a169d7a0ec6398e89"
973
973
  }
974
974
  ]
975
975
  },
@@ -979,7 +979,7 @@
979
979
  "hashes": [
980
980
  {
981
981
  "alg": "SHA-256",
982
- "content": "d5c03890b2b3a34277c73a14d3d7c97741dec1d6c382e87ee4cc6a5a52d938d7"
982
+ "content": "7cd05898db005ad19fbaeb25af3d958d47946f5db195e6969009fd45953e3e30"
983
983
  }
984
984
  ]
985
985
  },
@@ -989,7 +989,7 @@
989
989
  "hashes": [
990
990
  {
991
991
  "alg": "SHA-256",
992
- "content": "e2fb5fb869005b05d13fcc52da7ded281bb6758250a1cdf695dace286029d791"
992
+ "content": "4c80cbbae3c4acfb61e4cd0dfbf97e873ef7bded9d41ecaf84959e6fe5e0640f"
993
993
  }
994
994
  ]
995
995
  },
@@ -1239,7 +1239,7 @@
1239
1239
  "hashes": [
1240
1240
  {
1241
1241
  "alg": "SHA-256",
1242
- "content": "8b93949ce4f671d932c5389050db3a3e2750efcbed69681d69f392ccf4d2a168"
1242
+ "content": "1c8f8e6ffad01ebdb1bcf6fce6cfb6baae6fe40a9ce377b3894f8addc06c8180"
1243
1243
  }
1244
1244
  ]
1245
1245
  },
@@ -1249,7 +1249,17 @@
1249
1249
  "hashes": [
1250
1250
  {
1251
1251
  "alg": "SHA-256",
1252
- "content": "6fdacc0ee6c1d2b00029da8d300a574c051895ee98a9c88c9509a39b9bdccc71"
1252
+ "content": "91fe091ed7ab8f38340b8ac0fd3194c90b357cb9bab381a86594ac96600ced97"
1253
+ }
1254
+ ]
1255
+ },
1256
+ {
1257
+ "type": "file",
1258
+ "name": "templates/.claude/skills/feature-adr/scripts/expected-target-grammar.mjs",
1259
+ "hashes": [
1260
+ {
1261
+ "alg": "SHA-256",
1262
+ "content": "e763efd3af9c7ba71f758f6d6c349b21f2a41ded0420fc165269eab82b6433fb"
1253
1263
  }
1254
1264
  ]
1255
1265
  },
@@ -1263,6 +1273,16 @@
1263
1273
  }
1264
1274
  ]
1265
1275
  },
1276
+ {
1277
+ "type": "file",
1278
+ "name": "templates/.claude/skills/feature-adr/scripts/native-post-ci-evidence.mjs",
1279
+ "hashes": [
1280
+ {
1281
+ "alg": "SHA-256",
1282
+ "content": "b19b5491cb42f1e9f2fdefd642e7938640d68fd8bdff991f66fdcebef61f55ae"
1283
+ }
1284
+ ]
1285
+ },
1266
1286
  {
1267
1287
  "type": "file",
1268
1288
  "name": "templates/.claude/skills/frontend-design/LICENSE.txt",
@@ -20,9 +20,11 @@ $ARGUMENTS
20
20
  - Output: `01_task_brief.md`
21
21
  - ⏸️ **CHECKPOINT 1:** Подтверждение Task Brief + выбор verification mode
22
22
 
23
- 4. **Phase 2: Verified Research** (goap-research-ed25519)
23
+ 4. **Phase 2: Research** (goap-research-ed25519)
24
24
  - Используй `scripts/goap_planner.py` и `scripts/ed25519_verifier.py`
25
- - Режим: выбранный на CHECKPOINT 1 (moderate/strict/paranoid)
25
+ - По умолчанию `moderate`; `strict`/`paranoid` выбираются только при подтверждённых `issuer_keys_available` и `source_class_verified`
26
+ - Если planner вернул `PlanNotFound`, покажи verdict и причину и остановись до Solve
27
+ - Статус подписи издателя бери из `verify_fact`, а целостность цепочки — отдельно из `verify_citation_chain`
26
28
  - Output: `02_research_findings.md`
27
29
  - ⏸️ **CHECKPOINT 2:** Подтверждение findings
28
30
 
@@ -35,7 +35,7 @@ Scripts и references для Phase 2 находятся в:
35
35
 
36
36
  **Контрольные точки:** После каждой фазы скилл останавливается для подтверждения. Пользователь может скорректировать направление или продолжить.
37
37
 
38
- **Cryptographic Verification:** Ed25519 подписи для верификации источников (через goap-research-ed25519).
38
+ **Проверка происхождения:** Research фиксирует, какие источники были прочитаны и какие утверждения ими поддержаны. Ed25519 проверяет подпись издателя только при наличии настоящих ключей издателя и источника с подписанной выдачей. Подпись подтверждает происхождение конкретных байтов, а не истинность утверждения.
39
39
 
40
40
  ## Workflow Architecture
41
41
 
@@ -52,9 +52,9 @@ Scripts и references для Phase 2 находятся в:
52
52
  │ → Output: Task Brief │
53
53
  │ → ⏸️ CHECKPOINT 1: "Подтвердите Task Brief" │
54
54
  │ ↓ │
55
- │ Phase 2: RESEARCH (Ed25519 Verified) │
55
+ │ Phase 2: RESEARCH (GOAP + статус доказательств) │
56
56
  │ → Загрузи: ../goap-research-ed25519/SKILL.md │
57
- │ → Output: Verified Research Findings │
57
+ │ → Output: Research Findings + Verification Status │
58
58
  │ → ⏸️ CHECKPOINT 2: "Подтвердите findings" │
59
59
  │ ↓ │
60
60
  │ Phase 3: SOLVE │
@@ -105,57 +105,82 @@ Scripts и references для Phase 2 находятся в:
105
105
  [показать Task Brief]
106
106
 
107
107
  Варианты действий:
108
- • "ок" — перейти к Verified Research (moderate mode)
109
- • "режим strict" — research с порогом 0.95
110
- • "режим paranoid" — research с порогом 0.99
108
+ • "ок" — перейти к Research (moderate mode)
111
109
  • "скорректируй X" — внести изменения
112
110
 
113
111
  Что выберете?
114
112
  ═══════════════════════════════════════════════════════════════
115
113
  ```
116
114
 
115
+ Показывай дополнительные варианты `режим strict` и `режим paranoid` только после проверки **обоих** фактов для выбранного класса источников: `issuer_keys_available` (реальные закреплённые ключи издателей) и `source_class_verified` (источники действительно отдают подписанные данные). Если пользователь просит один из этих режимов без подтверждения хотя бы одного факта, назови недостающий факт и остановись на CHECKPOINT 1. Предложи `moderate` отдельно; не подменяй выбор молча. Для обычных PubMed/PMC/DOI/WHO эти факты не установлены.
116
+
117
+ | Ключи издателя | Подписанная выдача | Можно предложить `strict`/`paranoid`? |
118
+ |---|---|---|
119
+ | Нет | Нет | Нет; штатный выбор — `moderate`. |
120
+ | Да | Нет | Нет; назови недостающую подписанную выдачу. |
121
+ | Нет | Да | Нет; назови недостающие ключи издателя. |
122
+ | Да | Да | Да, после явного выбора пользователя. |
123
+
117
124
  ### Phase 2: Research (goap-research-ed25519)
118
125
 
119
126
  **Загрузи:** Прочитай `../goap-research-ed25519/SKILL.md` и выполни по его инструкциям.
120
127
 
121
128
  **Verification Modes (выбор на CHECKPOINT 1):**
122
129
 
123
- | Mode | Threshold | Команда на checkpoint |
124
- |------|-----------|----------------------|
125
- | `moderate` | 0.85 | "ок" или "продолжай" |
126
- | `strict` | 0.95 | "режим strict" |
127
- | `paranoid` | 0.99 | "режим paranoid" |
130
+ | Mode | Threshold | Условие |
131
+ |------|-----------|---------|
132
+ | `moderate` | 0.85 | Штатный режим по "ок" или "продолжай"; неподписанные факты остаются явно отмеченными. |
133
+ | `strict` | 0.95 | Только когда установлены оба capability-факта и пользователь выбрал режим. |
134
+ | `paranoid` | 0.99 | То же условие; более сильные требования к избыточности источников. |
135
+
136
+ Перед исследованием передай установленные capability-факты в `GOAPResearchPlanner`; не выставляй флаги `True` по имени сайта, наличию URL или желанию получить высокий confidence. Обрабатывай ответ как `ResearchPlan` **или** `PlanNotFound`, не как `None`:
137
+
138
+ - `PlanNotFound` с `GOAL_UNREACHABLE`: покажи verdict, причину и недостающие факты; остановись на research checkpoint. Увеличение бюджета поиска не исправит недоступные подписи.
139
+ - `PlanNotFound` с `SEARCH_EXHAUSTED`: покажи verdict и причину; остановись на research checkpoint. Можно предложить ограниченный повторный поиск, но не объявлять план найденным.
140
+ - Только `ResearchPlan` разрешает перейти к сбору находок; Phase 3: Solve доступна после успешного CHECKPOINT 2. `ResearchPlan` сам по себе не доказывает подписи или истинность фактов.
128
141
 
129
- **Output — Verified Research Findings:**
142
+ Если получен `PlanNotFound`, вместо шаблона «Research Findings Ready» покажи отдельный CHECKPOINT 2: «Research остановлен», фактический verdict, сообщение planner и недостающие capability-факты либо исчерпанный бюджет поиска. Предложи новый явный выбор: перейти на `moderate` при `GOAL_UNREACHABLE` из-за недоступной подписи, исправить интеграцию подписанной выдачи или сузить цель/повторить поиск при `SEARCH_EXHAUSTED`. Не переходи к Solve и не заполняй успешный research-шаблон на этом пути.
143
+
144
+ Для каждого использованного факта запиши результат `Ed25519Verifier.verify_fact`: `verified`, `trust_class` и причину отказа. Отдельно запиши результат `verify_citation_chain`; целостность цепочки исследователя не превращает `SELF_ATTESTED` в подпись издателя. Считай `issuer-verified` только факты с `verified=true` и `trust_class=ISSUER_SIGNED`; `SELF_ATTESTED` и `UNVERIFIED` показывай отдельно. Confidence, наличие DOI/URL и подпись исследователя не повышают класс до `ISSUER_SIGNED`. Если проверка не запускалась, напиши «не проверено», не ноль успешных или зелёный статус.
145
+
146
+ **Output — Research Findings:**
130
147
  ```markdown
131
- ## Verified Research Findings
148
+ ## Research Findings
132
149
 
133
150
  ### Verification Status
134
151
  - **Mode:** [selected_mode] ([threshold] threshold)
135
- - **Chain Integrity:** ✅ VERIFIED (ed25519)
136
- - **Unsigned Claims:** 0
137
- - **Total Citations:** [число]
152
+ - **Source access:** [сколько источников прочитано; сколько только из выдачи; сколько не открыто]
153
+ - **Issuer provenance:** [issuer-verified X/N; SELF_ATTESTED Y/N; UNVERIFIED Z/N из результатов verify_fact]
154
+ - **Citation-chain integrity:** [verified / failed / not checked по verify_citation_chain]
155
+ - **Claims without issuer verification:** [идентификаторы и причины; если нет — 0 по результатам]
156
+ - **Total citations:** [из списка фактически использованных ссылок]
138
157
 
139
- ### Verified Findings
140
- [Findings с signed citations]
158
+ ### Findings
159
+ [Утверждение, ссылка, evidence class и его фактический trust class]
141
160
 
142
161
  ### Confidence Assessment
143
- - **Cryptographically Verified (≥0.95):** [список]
144
- - **Cross-Reference Verified (0.85-0.95):** [список]
162
+ - **Confidence:** [оценки с отдельным основанием; порог не заменяет подпись]
145
163
 
146
164
  ### Citation Chain
147
- [Ed25519 verified chain — см. Appendix]
165
+ [Что именно проверено, каким ключом и с каким результатом; если не запускалось — not checked]
148
166
  ```
149
167
 
168
+ | Состояние результатов | Допустимая формулировка |
169
+ |---|---|
170
+ | Нет `ISSUER_SIGNED` с `verified=true` | «Источники процитированы; криптографическая проверка издателя не установлена». Укажи `SELF_ATTESTED` и `UNVERIFIED` отдельно. |
171
+ | Часть фактов issuer-verified | «Подпись издателя подтверждена для X из N фактов». Перечисли остальные факты и причины; общего «все проверено» нет. |
172
+ | Все факты issuer-verified | «Подпись издателя подтверждена для N из N фактов» **только после** фактического `verify_fact` для каждого факта. Целостность цепочки заявляй лишь при отдельном успешном `verify_citation_chain`; истинность фактов из подписи не следует. |
173
+
150
174
  **⏸️ CHECKPOINT 2:**
151
175
  ```
152
176
  ═══════════════════════════════════════════════════════════════
153
- ⏸️ CHECKPOINT 2: Verified Research Complete
177
+ ⏸️ CHECKPOINT 2: Research Findings Ready
154
178
 
155
179
  **Verification Status:**
156
- - Mode: [strict/moderate/paranoid]
157
- - Unsigned Claims: 0 ✅
158
- - Citation Chain: VERIFIED (Ed25519) ✅
180
+ - Mode: [фактически выбранный режим]
181
+ - Issuer provenance: [X/N issuer-verified; Y self-attested; Z unverified]
182
+ - Citation-chain integrity: [verified / failed / not checked]
183
+ - Неустановленные элементы: [факты и причины]
159
184
 
160
185
  **Ключевые находки:**
161
186
  [краткое summary]
@@ -165,7 +190,7 @@ Scripts и references для Phase 2 находятся в:
165
190
 
166
191
  Варианты действий:
167
192
  • "ок" — перейти к Solution
168
- • "исследуй глубже X" — дополнительный verified research
193
+ • "исследуй глубже X" — дополнительный research с новыми источниками
169
194
  • "повысь confidence для Y" — найти больше источников
170
195
 
171
196
  Что выберете?
@@ -210,8 +235,9 @@ Scripts и references для Phase 2 находятся в:
210
235
  [краткое summary]
211
236
 
212
237
  **Data Quality:**
213
- - Все рекомендации основаны на verified research ✅
214
- - Citation chain integrity: VERIFIED (Ed25519) ✅
238
+ - Основание рекомендаций: [перечень подтверждённых и неподтверждённых находок]
239
+ - Issuer provenance: [X/N фактов, по результатам verify_fact]
240
+ - Citation-chain integrity: [отдельный результат verify_citation_chain]
215
241
 
216
242
  Варианты действий:
217
243
  • "ок" — создать все документы
@@ -228,10 +254,11 @@ Scripts и references для Phase 2 находятся в:
228
254
  ## Strategic Analysis: [Название задачи]
229
255
 
230
256
  ### Executive Summary
231
- ### Verification Status (Ed25519)
257
+ ### Verification Status
232
258
  - Research Mode: [mode]
233
- - All Claims Verified: ✅
234
- - Citation Chain: CRYPTOGRAPHICALLY VERIFIED ✅
259
+ - Issuer provenance: [X/N фактов; причины для остальных]
260
+ - Citation-chain integrity: [verified / failed / not checked]
261
+ - Source support: [какие утверждения подтверждены прочитанными источниками; какие нет]
235
262
 
236
263
  ### Key Research Insights
237
264
  ### Recommended Strategy
@@ -258,17 +285,18 @@ Scripts и references для Phase 2 находятся в:
258
285
  | Документ | Файлы |
259
286
  |----------|-------|
260
287
  | Task Brief | `01_task_brief.md`, `01_task_brief.docx` |
261
- | Verified Research | `02_research_findings.md`, `02_research_findings.docx` |
288
+ | Research Findings | `02_research_findings.md`, `02_research_findings.docx` |
262
289
  | Solution | `03_solution.md`, `03_solution.docx` |
263
290
  | Final Summary | `04_final_summary.md`, `04_final_summary.docx` |
264
291
 
265
292
  ## Quality Standards
266
293
 
267
294
  **Anti-Hallucination Checks:**
268
- - [ ] 100% claims имеют citations
269
- - [ ] Citation chain cryptographically verified (Ed25519)
270
- - [ ] Нет утверждений без DOI/URL
271
- - [ ] Confidence levels для всех findings
295
+ - [ ] Для каждого существенного утверждения указан источник либо явно сказано, что подтверждения нет.
296
+ - [ ] Статус `ISSUER_SIGNED` взят из результата `verify_fact`, а не из confidence, режима или подписи исследователя.
297
+ - [ ] Статус цепочки взят отдельно из `verify_citation_chain`; если проверка не запускалась, это написано.
298
+ - [ ] Утверждения без issuer verification перечислены, а `SELF_ATTESTED` не названы подписями издателя.
299
+ - [ ] Confidence и ограничения источника показаны отдельно от криптографического происхождения.
272
300
 
273
301
  **Completeness:**
274
302
  - [ ] Все 9 модулей problem-solver пройдены
@@ -278,7 +306,7 @@ Scripts и references для Phase 2 находятся в:
278
306
 
279
307
  **Required skills** (installed as sibling skill directories alongside this skill):
280
308
  - `explore` — task clarification and brief generation
281
- - `goap-research-ed25519` — verified research with Ed25519 (includes scripts/ and references/)
309
+ - `goap-research-ed25519` — source-aware research with optional issuer-signature verification (includes scripts/ and references/)
282
310
  - `problem-solver-enhanced` — 9-module problem solving with TRIZ
283
311
 
284
312
  **Runtime:**
@@ -245,10 +245,21 @@ task that implements it: a wrapped continuation line of a list item is not read
245
245
  passed would fail this reader, so a plan written before that date may be red on a re-check — move
246
246
  the citation onto a task line, or re-check that one plan with `--no-require-task-lines` and say so
247
247
  in the checkpoint banner.
248
+ **Literal target declaration:** K2, native source admission and core plan authority read raw text.
249
+ Use exactly one column-zero uppercase `EXPECTED_CODE_TARGETS:` marker (ASCII spaces/tabs after
250
+ it are allowed), followed through EOF by nonempty `- <repo-relative path>` rows and optional
251
+ trim()-empty blank lines. Use LF throughout the declaration; CR/CRLF, another marker, headings,
252
+ prose, fences/comments or alternate/indented bullets after it refuse the whole block. Final LF is
253
+ optional. Markers inside examples/comments count too; this is not a Markdown authenticity check.
254
+ Payloads stay literal: no trimming, backtick removal, slash rewriting or `./` repair. A well-formed
255
+ block still owes the existing path checks; neither malformed grammar nor a rejected/rewritten
256
+ plan path establishes a valid subset. Explicit target args retain their separate normalization
257
+ and precedence; coder text supplies diagnostics only.
258
+
248
259
  What it checks: C1 every ADR **decision** (a `# ADR-NNN` / `## ADR-NNN` heading INSIDE the file, not
249
260
  just the filename prefix — a file with several headings owes several plan citations) has a plan task
250
261
  citing it · C2 every ADR Confirmation test path is named in the plan · C3 the `EXPECTED_CODE_TARGETS:`
251
- block parses line by line · C4 the feature's declared acid corpus is named · C5 (WARN) the
262
+ block satisfies the literal trailing grammar and every path passes C3 · C4 the feature's declared acid corpus is named · C5 (WARN) the
252
263
  `Inputs read:` line · C8 every requirement id declared in `01_requirements.md` (`FR-N`, `NFR-N`,
253
264
  `AC-N`, `C-N`) is cited by the plan by word boundary — WARN with a count by default, FAIL per missing
254
265
  id under `--require-requirements` (the pipeline passes this flag; a bare interactive run of the
@@ -60,6 +60,16 @@ retries/re-entry. Refuse a failed initialization before edits. Admission does no
60
60
  This is executable Plain guidance with mandatory orchestration, not an automatic Plain dispatcher.
61
61
  An unrelated process can change source after admission; ordinary final QE still checks freshness.
62
62
 
63
+ The plan's target declaration uses the same literal grammar as K2: exactly one raw column-zero
64
+ uppercase `EXPECTED_CODE_TARGETS:` marker, optional ASCII marker padding, LF-only declaration,
65
+ nonempty dash-space rows and trim()-empty blank lines through EOF. A later heading, prose,
66
+ fence/comment delimiter, alternate bullet, CR or second marker refuses the whole block.
67
+ Raw markers in examples/comments count; no Markdown authenticity is inferred. Path payloads
68
+ are not unquoted, trimmed or repaired. Native containment, symlink/circular exclusions, guard
69
+ provenance, bounded reads and history remain separate authority checks. Core establishes no
70
+ plan targets if grammar or any applicable literal path check fails; coder output remains
71
+ advisory and explicit target args retain their separate precedence and normalization.
72
+
63
73
  ### Current literal context (plain and delegated coding)
64
74
 
65
75
  Before coding or delegating, run the installed helper beside this module. Set `CONTEXT_HELPER` to
@@ -205,4 +215,4 @@ Tasks completed: {completed}/{total}
205
215
  - [ ] ADR decisions reflected in code
206
216
  - [ ] No unintended side effects on existing functionality
207
217
  - [ ] Change manifest is complete and accurate
208
- - [ ] No TODO/FIXME/HACK left without justification
218
+ - [ ] No unfinished-work or workaround markers left without justification
@@ -10,7 +10,7 @@ dz qe-rounds --source native --action prepare --project "$REPO" --feature-dir "$
10
10
 
11
11
  Bind the actual selected families and use the returned full roster, nonce and revision in the existing QE receipt at `feature/.fa-state/review-convergence-qe-review.json`. An owner-approved family exception must use `owner-exception` and preserve its explicit approval. Call `dz qe-rounds --source native --action evaluate --project "$REPO" --feature-dir "$FEATURE" --json` after actual independent review; missing or incompatible installed core/helper capabilities are NOT ESTABLISHED. No project-local module search, helper override, automatic installation or fake bridge evidence is allowed.
12
12
 
13
- Native history fixes the ceiling at three witnessed cycles, including partial and unresolved originating reviews. Multiple roles, fallback, repeated prepare/evaluate and restart share one cycle. Prepare may refresh artifacts before a review originates. After witnessing, a measured code/design change requires explicit `--action begin-repair` with the actual roster before another review; unchanged third-cycle completion may finish, while a fourth opening is refused. The ceiling applies to formal feature review as well as focused roles. Do not evade it by changing family, host, source or nonce. No-verdict attempts are reported separately. Native `--ceiling` is refused.
13
+ Native history keeps the default ceiling at three witnessed cycles, including partial and unresolved originating reviews. Multiple roles, fallback, repeated prepare/evaluate and restart share one cycle. Prepare may refresh artifacts before a base-cycle review originates; an admitted continuation target is already pinned. After witnessing, a measured code/design change requires explicit `--action begin-repair` with the actual roster before another review; unchanged third-cycle completion may finish, while an ordinary fourth opening is refused. Explicit `begin-continuation` is the one-use owner-bound exception described below; it pins the fourth target before review and never permits cycle five. The ceiling applies to formal feature review as well as focused roles. Do not evade it by changing family, host, source or nonce. No-verdict attempts are reported separately. Native `--ceiling` is refused.
14
14
 
15
15
  If bytes change after an independent receipt arrives but before its first evaluation, call `--action evaluate` to retain the receipt's valid origins and conditions against its admitted old snapshot. The result remains `not-established` with `native-repair-required`; it never closes current bytes or the historical checkpoint. Then call `--action begin-repair` with the actual roster and obtain fresh independent verification of the new snapshot, including the author delta and original conditions. A missing or invalid origin cannot witness a cycle or authorize repair. Keep the surviving receipt and native authority intact through recovery.
16
16
 
@@ -646,3 +646,22 @@ Verdict: {READY | NEEDS FIXES | CONDITIONAL}
646
646
  - [ ] Gap detection loop completed with zero remaining gaps (M+)
647
647
  - [ ] QCSD quality risks from Step 3.5 verified as mitigated (M+)
648
648
  - [ ] QE report created and complete
649
+
650
+ Native QE keeps its default ceiling at three. `dz qe-rounds --source native --action begin-continuation --project <root> --slug <feature> --reviewers <actual-roster-json> --owner-approval <inline-json>` can admit exactly one additional cycle after three witnessed cycles whose active cycle has never closed. A second ordinary continuation refuses. The target is pinned before its fourth origin; prepare and repair cannot redirect it. Fresh independent receipts, original reviewer conditions, author delta assessment and checkpoint verification still govern closure. An admitted checkpoint remains required if its file is removed or replaced; restore the original checkpoint and obtain independent verification before closure. Prepare retains its digest, and replay rejects a closed continuation without matching checkpoint evidence.
651
+
652
+ The exact flat declaration has schema `native-qe-owner-approval-1`, a UUIDv4 `approvalId`, nonempty `owner` (≤256 characters), `decision` (≤2048), `evidenceSource` (≤4096), canonical `projectRoot` and `featurePath`, `journalId`, `sequence`, `eventDigest`, `cycleId`, prior `revision` and distinct measured `targetRevision`. Native read-only reporting exposes the predecessor event digest. Inline JSON and its serialized declaration are capped at 16 KiB. Obtain the changed target digest from the bounded artifact manifest; consent must explicitly bind it. No helper fills missing consent or updates stale bindings. Exact committed retries can return the admitted cycle without appending.
653
+
654
+ This is surviving local owner evidence, not human authentication or acceptance. Core and the CLI-owned installed helper require continuation capability 1 while legacy APIs remain 1. Older readers reject the extended event explicitly. Install only accepted, signed local packed candidates before authoritative use; no npm publication is required. Journal/head/host publication remains conservatively crash detecting under the existing lock, with no filesystem atomicity claim.
655
+
656
+
657
+ ### One post-CI correction after CLOSED fourth
658
+
659
+ The separate action is `dz qe-rounds --source native --action begin-post-ci-correction --project <root> --slug <feature> --reviewers <original-roster-json> --owner-approval <strict-inline-json> --correction-evidence <repo-relative-json-file>`. Both installed capabilities must advertise post-CI version 2; history and ordinary continuation APIs remain 1. The current fourth event must be its witnessed CLOSED evaluation. This opens one pinned fifth in the same journal; it cannot be redirected, renewed, repaired into a sixth or substituted by another continuation. Historical fourth receipts remain history.
660
+
661
+ The declaration keeps the thirteen existing owner bindings, changes schema to `native-qe-post-ci-owner-approval-1`, and requires `evidenceDigest` for the exact canonical descriptor bytes. Descriptor schemas are legacy `native-qe-post-ci-evidence-1` and imported-history `native-qe-post-ci-evidence-2`; compact JSON plus LF, strict finite keys, max128 KiB, no duplicate keys. All referenced immutable proof belongs under the exact feature `qa/post-ci-correction/` namespace. The correction QA namespace must contain exactly its descriptor, referenced proofs and digest-bound `excludedEvidence` supplements, including tracked, untracked and ignored files. Extra entries or changed/missing listed proof block availability; keep inline consent and evolving native/review outputs outside this immutable namespace. Complete fifth receipts may list reviewers in any order; durable closure preserves the original roster order. Archive complete failed CI output with its actual canceled/failing conclusion, exact fixture patch, accepted integration provenance, both successful full baseline modes and the complete successful CI policy/check set. Separate raw logs allow16 MiB each, four distinct logs/64 MiB total using bounded streaming; source/journal/host/manifest/deadline limits do not increase. Hashes and locally asserted policy do not authenticate remote CI or owner identity.
662
+
663
+ Every original roster role must independently inspect the actual full scope, imported merge resolutions, qualification and unchanged dependencies. Each current review entry adds `postCiEvidenceVerification` with exact keys `{schema,evidenceDigest,revision,nonce,anchorSequence,anchorEventDigest,supported,evidence}`; schema `native-qe-post-ci-verification-1`, supported=true, with actual nonempty independent evidence. Read reports descriptor and current anchor through `postCiCorrection`. The initial anchor is fifth admission; a durably recorded missing/replaced proof or checkpoint evaluation advances it. A generic clean or delta/checkpoint verification cannot replace this extension. Ordinary/legacy reviews reject the unexpected extension.
664
+
665
+ A missing/replaced required proof or checkpoint blocks acceptance. Mutating evaluate records `native-post-ci-proof-unavailable` without origins, preserving cycle, target, conditions and all history. Restoration plus old same-target receipts remains blocked; fresh per-role proofs must bind the current anchor. Read and refused prepare do not append, and no operation can remember an unobserved loss/restoration. Closure independently binds the complete preserved roster via `postCiEvidenceClosure`; checkpoint closure remains conjunctive. Accept this mechanism's independent QE and required CI, then install its exact signed local packages before any original-feature fifth admission. No global activation or npm publication is implied.
666
+
667
+ Schema 2 (`native-qe-post-ci-evidence-2`) adds exact `scope.importedArtifacts` rows (`path`, absent `before`, SHA256 `digest`, byte `size`) for foreign feature history imported by a normal two-parent merge. Schema 1 remains supported with its original closed shape. Selected foreign roots must be absent from the failed/base/preimport trees and have identical complete membership and bytes in accepted, integrated, qualified and current views. Ignored or untracked extras, active-feature/authority paths, source/proof overlap, links and file/directory aliases refuse admission. Imported support shares the existing 2 MiB/file and 16 MiB source aggregate; schema 2 jointly bounds immutable proof/support to 256 files and 256 derived directories. Source manifests remain limited to 256 entries. Support is reobserved by core; recorded loss requires fresh bound originating reviews after restoration.
@@ -101,6 +101,7 @@ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
101
101
  import { execFileSync } from 'node:child_process';
102
102
  import { createHash } from 'node:crypto';
103
103
  import { isAbsolute, join, resolve } from 'node:path';
104
+ import { parseExpectedTargetBlock } from './expected-target-grammar.mjs';
104
105
 
105
106
  const argv = process.argv.slice(2);
106
107
  const acidArg = argv.find((a) => a.startsWith('--acid='));
@@ -151,8 +152,8 @@ if (!existsSync(FDIR)) notEstablished(`feature dir absent: ${FDIR}`);
151
152
  if (!existsSync(planPath)) notEstablished('06_implementation_plan.md absent');
152
153
  const plan = readFileSync(planPath, 'utf-8');
153
154
  if (plan.trim().length < 200) notEstablished('plan suspiciously small (<200 chars)');
154
- // The EXPECTED_CODE_TARGETS block as C3 reads it (C3 below reuses THIS match — one reader).
155
- const blockM = plan.match(/EXPECTED_CODE_TARGETS:\s*\n((?:\s*[-*]\s*.+\n?)+)/);
155
+ // C3 and task exclusion share the raw trailing declaration boundary, even on malformed input.
156
+ const targetBlock = parseExpectedTargetBlock(plan);
156
157
 
157
158
  // `## Amendments` as C6 reads it: up to three leading spaces, two to four hashes, trailing text
158
159
  // allowed; the section ends at the next level-1..4 heading. Returned as LINE indices over masked
@@ -179,15 +180,11 @@ function findAmendmentsSection(maskedLines) {
179
180
  function planTaskText(planText) {
180
181
  const lines = maskMarkdown(planText, { unclosed: 'mask' }).split('\n');
181
182
  const am = findAmendmentsSection(lines);
182
- let tFrom = -1, tTo = -1;
183
- if (blockM) {
184
- tFrom = planText.slice(0, blockM.index).split('\n').length - 1;
185
- tTo = planText.slice(0, blockM.index + blockM[0].length - 1).split('\n').length - 1;
186
- }
183
+ const tFrom = targetBlock.startLine;
187
184
  const keep = [];
188
185
  for (let i = 0; i < lines.length; i++) {
189
186
  if (am.head >= 0 && i >= am.head && i < am.end) continue;
190
- if (tFrom >= 0 && i >= tFrom && i <= tTo) continue;
187
+ if (tFrom >= 0 && i >= tFrom) continue;
191
188
  const l = lines[i];
192
189
  if (/^ {0,3}#{1,6}\s/.test(l) || /^\s*(?:[-*+]|\d+[.)])\s+/.test(l) || /^\s*\|/.test(l)) keep.push(l);
193
190
  }
@@ -408,14 +405,11 @@ function classifyTargetPath(path) {
408
405
 
409
406
  // C3 — EXPECTED_CODE_TARGETS block, line-level validation
410
407
  const listedTargets = new Set();
411
- if (!blockM) failures.push('C3: no EXPECTED_CODE_TARGETS: block in the plan');
408
+ if (!targetBlock.ok) failures.push(`C3: EXPECTED_CODE_TARGETS declaration refused — ${targetBlock.reason}`);
412
409
  else {
413
- const lines = blockM[1].split('\n').map(s => s.trim()).filter(Boolean);
414
- if (lines.length === 0) failures.push('C3: EXPECTED_CODE_TARGETS block is empty');
415
- for (const ln of lines) {
416
- const path = ln.replace(/^[-*]\s*/, '').replace(/`/g, '').trim();
410
+ for (const path of targetBlock.targets) {
417
411
  const reasons = classifyTargetPath(path);
418
- if (reasons.length) failures.push(`C3: target line rejected: "${safe(ln)}" — ${reasons.join(', ')}`);
412
+ if (reasons.length) failures.push(`C3: target line rejected: "${safe(path)}" — ${reasons.join(', ')}`);
419
413
  else listedTargets.add(path);
420
414
  }
421
415
  }
@@ -12,6 +12,8 @@ import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, open
12
12
  import { dirname, relative, resolve, sep } from 'node:path'
13
13
  import { execFileSync } from 'node:child_process'
14
14
  import { fileURLToPath } from 'node:url'
15
+ import { parseExpectedTargetBlock } from './expected-target-grammar.mjs'
16
+ import { measureNativePostCiEvidence } from './native-post-ci-evidence.mjs'
15
17
 
16
18
  export const SCHEMA = 'fa-review-convergence-1'
17
19
  const phases = ['ideation', 'qe']
@@ -34,9 +36,9 @@ function checkpointProof(p) {
34
36
  return dictionary(p, ['checkpointDigest', 'evidence', 'supported', 'route', 'findings', 'reportDigest']) && hash(p.checkpointDigest) && text(p.evidence) && p.supported === true && ['native', 'mode-b', 'fallback'].includes(p.route) && array(p.findings) && p.findings.every(v => dictionary(v, ['digest', 'conditionId']) && hash(v.digest) && text(v.conditionId)) && (p.reportDigest === null || hash(p.reportDigest))
35
37
  }
36
38
  function reviewerFamily(stage) { return stage.qeReviewerUsed === 'codex-fallback' ? 'codex' : stage.qeReviewerUsed }
37
- function reviewerEntry(r, roster) {
39
+ function reviewerEntry(r, roster, postCi = false) {
38
40
  const owners = roster.map(p => p.id)
39
- return !(!dictionary(r, ['reviewer', 'family', 'independent', 'verdict', 'conditions', 'verifications', 'newRisks', 'authorAssessmentChecked', 'deltaVerification', 'checkpointVerification'], ['reviewer', 'family', 'independent', 'verdict', 'conditions', 'verifications', 'newRisks', 'authorAssessmentChecked', 'deltaVerification']) || !owners.includes(r.reviewer) || roster.find(p => p.id === r.reviewer)?.family !== r.family || r.independent !== true || !['clean', 'conditional', 'no-go'].includes(r.verdict) || !array(r.conditions) || !r.conditions.every(c => condition(c, owners) && c.owner === r.reviewer) || !array(r.verifications) || !risks(r.newRisks, owners) || !r.newRisks.conditions.every(c => c.owner === r.reviewer) || typeof r.authorAssessmentChecked !== 'boolean')
41
+ return !(!dictionary(r, ['reviewer', 'family', 'independent', 'verdict', 'conditions', 'verifications', 'newRisks', 'authorAssessmentChecked', 'deltaVerification', 'checkpointVerification', ...(postCi ? ['postCiEvidenceVerification'] : [])], ['reviewer', 'family', 'independent', 'verdict', 'conditions', 'verifications', 'newRisks', 'authorAssessmentChecked', 'deltaVerification']) || !owners.includes(r.reviewer) || roster.find(p => p.id === r.reviewer)?.family !== r.family || r.independent !== true || !['clean', 'conditional', 'no-go'].includes(r.verdict) || !array(r.conditions) || !r.conditions.every(c => condition(c, owners) && c.owner === r.reviewer) || !array(r.verifications) || !risks(r.newRisks, owners) || !r.newRisks.conditions.every(c => c.owner === r.reviewer) || typeof r.authorAssessmentChecked !== 'boolean')
40
42
  }
41
43
  function result(phase, verdict, revision, reasons, unresolved = []) { return { schema: SCHEMA, phase, verdict, revision, unresolved, reasons } }
42
44
 
@@ -53,7 +55,7 @@ function hostProblem(host) {
53
55
  if (host.conditions.some(c => c.scope.some(path => !manifest.some(p => p.path === path)))) return 'host-condition-scope-unbound'
54
56
  return null
55
57
  }
56
- export function evaluateReviewConvergence(host, receipt, currentManifest, originOnly = false) {
58
+ export function evaluateReviewConvergence(host, receipt, currentManifest, originOnly = false, postCi = false) {
57
59
  const phase = host?.phase
58
60
  const refuse = reason => result(phase || 'qe', 'not-established', host?.snapshot?.revision || null, [reason], (array(host?.conditions) ? host.conditions : []).map(c => c?.id))
59
61
  const problem = hostProblem(host)
@@ -78,7 +80,7 @@ export function evaluateReviewConvergence(host, receipt, currentManifest, origin
78
80
  }
79
81
  }
80
82
  for (const r of receipt.reviews) {
81
- if (!reviewerEntry(r, host.reviewers)) return refuse('reviewer-contract-invalid')
83
+ if (!reviewerEntry(r, host.reviewers, postCi)) return refuse('reviewer-contract-invalid')
82
84
  if (Object.hasOwn(r, 'checkpointVerification') && !checkpointProof(r.checkpointVerification)) return refuse('checkpoint-verification-contract-invalid')
83
85
  if (receipt.author && !r.authorAssessmentChecked) return refuse('author-risk-assessment-not-reviewed')
84
86
  if (receipt.author) {
@@ -150,14 +152,9 @@ function pathsFor(repo, feature, phase, native = false) {
150
152
  if (phase === 'qe') {
151
153
  const planFile = safePath(repo, prefix + '/06_implementation_plan.md', false)
152
154
  const plan = native ? new TextDecoder('utf-8', { fatal: true }).decode(nativeBytes(planFile, 2 * 1024 * 1024)) : readFileSync(planFile, 'utf8')
153
- const block = plan.split(/^EXPECTED_CODE_TARGETS:\s*$/m)
154
- if (block.length !== 2) throw Error('target-block-missing-or-duplicate')
155
- const planned = block[1].split('\n').filter(line => line.trim()).map(line => {
156
- const match = /^- (.+)$/.exec(line)
157
- if (!match) throw Error('target-block-malformed')
158
- return match[1]
159
- })
160
- if (!planned.length) throw Error('target-block-empty')
155
+ const block = parseExpectedTargetBlock(plan)
156
+ if (!block.ok) throw Error(block.reason)
157
+ const planned = block.targets
161
158
  const baseFile = resolve(feature, '.fa-state/base-ref')
162
159
  const base = existsSync(baseFile) ? (native ? nativeBytes(baseFile, 256 * 1024).toString('utf8') : readFileSync(baseFile, 'utf8')).trim() : 'HEAD'
163
160
  if (!/^[A-Za-z0-9_./:-]+$/.test(base) || base.startsWith('-')) throw Error('base-ref-invalid')
@@ -165,13 +162,13 @@ function pathsFor(repo, feature, phase, native = false) {
165
162
  // Exact index membership includes staged additions/intent-to-add. The selected
166
163
  // base proves deletions; descendants and unrelated history confer no authority.
167
164
  const proof = [['ls-files', '--cached', '-z', '--', guard], ['ls-tree', '-r', '--name-only', '-z', base, '--', guard]].map(command =>
168
- execFileSync('git', command, { cwd: repo, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, ...(native ? { timeout: 2000, env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' } } : {}) }).split('\0').includes(guard))
165
+ execFileSync('git', native ? ['-c', 'diff.autoRefreshIndex=false', ...command] : command, { cwd: repo, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, ...(native ? { timeout: 2000, env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' } } : {}) }).split('\0').includes(guard))
169
166
  const allowedGuard = p => p === guard && proof.some(Boolean)
170
167
  if (planned.some(p => /^(features|\.dz|\.agentic-qe|roam)\//.test(p) && !allowedGuard(p))) throw Error('circular-review-target')
171
168
  paths.push(...planned)
172
169
  // Additions/deletions are discovered by the host, not the author delta request.
173
170
  for (const command of [['diff', '--no-renames', '--name-only', '-z', base, '--'], ['ls-files', '--others', '--exclude-standard', '-z']]) {
174
- const names = execFileSync('git', command, { cwd: repo, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, ...(native ? { timeout: 2000, env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' } } : {}) }).split('\0').filter(Boolean)
171
+ const names = execFileSync('git', native ? ['-c', 'diff.autoRefreshIndex=false', ...command] : command, { cwd: repo, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, ...(native ? { timeout: 2000, env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' } } : {}) }).split('\0').filter(Boolean)
175
172
  paths.push(...names.filter(p => allowedGuard(p) || !p.startsWith('features/') && !p.startsWith('.dz/') && !p.startsWith('.agentic-qe/') && !p.startsWith('roam/') && p !== 'architecture/map.json'))
176
173
  }
177
174
  }
@@ -332,6 +329,8 @@ export function runGate({ action, repo, feature, phase, reviewers }) {
332
329
 
333
330
  // Only the CLI supplies this capability. The default standalone runGate remains synchronous.
334
331
  export const NATIVE_REVIEW_API_VERSION = 1
332
+ export const NATIVE_REVIEW_CONTINUATION_API_VERSION = 1
333
+ export const NATIVE_REVIEW_POST_CI_CORRECTION_API_VERSION = 2
335
334
  function nativeNames(dir) {
336
335
  if (!existsSync(dir)) return []
337
336
  const st = lstatSync(dir)
@@ -366,18 +365,22 @@ function nativeManifest(repo, paths) {
366
365
  return { path, digest: bytes === null ? null : digest(bytes) }
367
366
  })
368
367
  }
369
- function nativeReceipt(host, receipt, manifest) {
368
+ function nativeReceipt(host, receipt, manifest, postCi = false) {
370
369
  // Reuse the unchanged strong aggregate gate with just the originating subset roster.
371
370
  // Missing reviewers can prevent closure without invalidating a structurally valid partial origin.
372
371
  if (!host || host.phase !== 'qe' || !dictionary(receipt, ['schema', 'phase', 'nonce', 'revision', 'reviews', 'author']) || receipt.schema !== SCHEMA || receipt.phase !== 'qe' || receipt.nonce !== host.snapshot.nonce || receipt.revision !== host.snapshot.revision || !array(receipt.reviews, 16) || !receipt.reviews.length || new Set(receipt.reviews.map(r => r?.reviewer)).size !== receipt.reviews.length) return null
373
- if (!receipt.reviews.every(r => reviewerEntry(r, host.reviewers))) return null
374
- const checked = evaluateReviewConvergence(host, receipt, manifest, true)
372
+ if (!receipt.reviews.every(r => reviewerEntry(r, host.reviewers, postCi))) return null
373
+ const checked = evaluateReviewConvergence(host, receipt, manifest, true, postCi)
375
374
  if (checked.verdict === 'not-established') return null
376
375
  return receipt.reviews.map(r => ({ reviewer: r.reviewer, family: r.family }))
377
376
  }
378
- export function runNativeReviewGate({ action, repo, feature, reviewers, api }) {
377
+ export function runNativeReviewGate({ action, repo, feature, reviewers, ownerApproval, correctionEvidence, api }) {
379
378
  if (!api || api.version !== 1 || typeof api.transact !== 'function') throw Error('native-core-capability-not-established')
380
- if (!['prepare', 'evaluate', 'begin-repair'].includes(action)) throw Error('native-action-invalid')
379
+ if (!['prepare', 'evaluate', 'begin-repair', 'begin-continuation', 'begin-post-ci-correction'].includes(action)) throw Error('native-action-invalid')
380
+ if (action === 'begin-continuation' && api.continuationVersion !== 1) throw Error('native-continuation-capability-not-established')
381
+ if (!['begin-continuation', 'begin-post-ci-correction'].includes(action) && ownerApproval !== undefined) throw Error('native-owner-approval-action-invalid')
382
+ if (action === 'begin-post-ci-correction' && (api.postCiCorrectionVersion !== 2 || typeof api.validateEvidence !== 'function')) throw Error('native-post-ci-capability-not-established')
383
+ if (action !== 'begin-post-ci-correction' && correctionEvidence !== undefined) throw Error('native-post-ci-evidence-action-invalid')
381
384
  repo = realpathSync(repo); feature = realpathSync(resolve(repo, feature))
382
385
  const prefix = relative(repo, feature).split(sep).join('/')
383
386
  if (!prefix || prefix.startsWith('../') || api.projectRoot !== repo || api.featurePath !== feature) throw Error('native-feature-binding-invalid')
@@ -420,8 +423,9 @@ export function runNativeReviewGate({ action, repo, feature, reviewers, api }) {
420
423
  if (!r.verifications.every(v => dictionary(v, ['id', 'revision', 'classification', 'evidence', 'implementationVerified']) && text(v.id) && hash(v.revision) && kinds.includes(v.classification) && text(v.evidence) && typeof v.implementationVerified === 'boolean') || new Set(r.verifications.map(v => v.id)).size !== r.verifications.length) throw Error('native-ideation-verification-invalid')
421
424
  }
422
425
  }
426
+ const nativeState = api.read?.(), postCi = Boolean(nativeState?.postCiCorrection?.used)
423
427
  let checkpoint = null
424
- if (observed[2] !== null) {
428
+ try { if (observed[2] !== null) {
425
429
  if (observed[2].length && observed[2][observed[2].length - 1] !== 10) throw Error('native-checkpoint-candidate-torn')
426
430
  for (const line of new TextDecoder('utf-8', { fatal: true }).decode(observed[2]).split('\n').filter(Boolean)) {
427
431
  const entry = JSON.parse(line)
@@ -429,11 +433,13 @@ export function runNativeReviewGate({ action, repo, feature, reviewers, api }) {
429
433
  if (entry.stage === 'qe') checkpoint = { checkpointDigest: digest(JSON.stringify(entry.result)), checkpoint: entry.result }
430
434
  }
431
435
  }
436
+ } catch (error) { if (!postCi) throw error }
432
437
  const directoryFence = () => ({ adr: nativeNames(safePath(repo, prefix + '/03_adr')), bridge: nativeNames(safePath(repo, prefix + '/.fa-state/qe-bridge')), fragments: nativeNames(stateDir).filter(n => /^native-qe-(history\.jsonl|head\.json)\..*\.tmp$/.test(n)) })
433
438
  const membership = directoryFence()
434
439
  const paths = pathsFor(repo, feature, 'qe', true).concat((host?.conditions || []).flatMap(c => c.scope))
435
440
  const manifest = nativeManifest(repo, paths)
436
441
  if (action !== 'evaluate' && (!array(reviewers, 16) || !reviewers.length || !reviewers.every(r => dictionary(r, ['id', 'family']) && text(r.id) && ['codex', 'claude', 'owner-exception'].includes(r.family)) || new Set(reviewers.map(r => r.id)).size !== reviewers.length)) throw Error('reviewers-invalid')
442
+ const correction = action === 'begin-post-ci-correction' ? measureNativePostCiEvidence({ repo, featurePath: prefix, evidencePath: correctionEvidence, previous: nativeState?.postCiCorrection?.evidence?.predecessorHost?.snapshot?.manifest || nativeState?.snapshot?.manifest, current: manifest, validate: api.validateEvidence }) : null
437
443
  const observationDigest = digest(JSON.stringify({ files: observed.slice(1).map(b => b === null ? null : digest(b)), membership, manifest }))
438
444
  const checkpointClosure = host && receipt && checkpoint ? reconcileCheckpoint(host, receipt, checkpoint, repo, feature, observed[7]) : null
439
445
  return api.transact(action, active => {
@@ -451,18 +457,19 @@ export function runNativeReviewGate({ action, repo, feature, reviewers, api }) {
451
457
  if (!active && (observed[1] !== null || host && (host.reviewSeen || host.rework || host.conditions.length) || checkpoint)) throw Error('native-surviving-review-evidence-refuses-admission')
452
458
  if (active && (!host || !same(host.snapshot, active.snapshot))) throw Error('native-host-admitted-snapshot-mismatch')
453
459
  const revision = digest(JSON.stringify(manifest))
454
- const priorOrigins = nativeReceipt(host, receipt, host?.snapshot.manifest)
455
- if (action === 'prepare' && host && host.snapshot.revision !== revision && (active?.witnessed || priorOrigins)) throw Error('native-repair-required')
460
+ const priorOrigins = nativeReceipt(host, receipt, host?.snapshot.manifest, Boolean(active?.postCiCorrection?.used))
461
+ if (action === 'prepare' && host && host.snapshot.revision !== revision && (active?.witnessed || priorOrigins) && !active?.continuation) throw Error('native-repair-required')
456
462
  if (action === 'evaluate') {
457
463
  if (!active || !host) throw Error('native-prepare-required')
458
464
  const drift = !same(manifest, host.snapshot.manifest)
459
465
  // A surviving receipt may witness admitted A after bytes become B. It cannot
460
466
  // accept B: retain A and its origins, then require a separate begin-repair.
461
467
  // Current bytes still participate in the observation digest and every fence.
462
- const origins = priorOrigins
468
+ const origins = active?.postCiCorrection?.used && priorOrigins ? host.reviewers.filter(r => priorOrigins.some(o => o.reviewer === r.id && o.family === r.family)).map(r => ({ reviewer: r.id, family: r.family })) : priorOrigins
463
469
  let verdict = drift ? result('qe', 'not-established', host.snapshot.revision, ['native-repair-required'])
464
- : receipt === null ? result('qe', 'not-established', revision, [receiptProblem || 'reviewer-receipt-missing']) : evaluateReviewConvergence(host, receipt, manifest)
470
+ : receipt === null ? result('qe', 'not-established', revision, [receiptProblem || 'reviewer-receipt-missing']) : evaluateReviewConvergence(host, receipt, manifest, false, Boolean(active?.postCiCorrection?.used))
465
471
  // Closure is deliberately stronger than originating partial/unresolved evidence.
472
+ if (verdict.verdict === 'closed' && active.continuation && active.checkpointDigest && checkpoint?.checkpointDigest !== active.checkpointDigest) verdict = result('qe', 'not-established', revision, ['native-continuation-checkpoint-unavailable-or-changed'])
466
473
  if (verdict.verdict === 'closed' && checkpoint && !checkpointClosure) verdict = result('qe', 'not-established', revision, ['historical-checkpoint-own-verification-missing'])
467
474
  const preserved = new Map(host.conditions.map(c => [c.id, c]))
468
475
  if (origins) {
@@ -471,15 +478,23 @@ export function runNativeReviewGate({ action, repo, feature, reviewers, api }) {
471
478
  }
472
479
  if (drift) verdict.unresolved = [...preserved.keys()]
473
480
  if (verdict.verdict === 'closed' && checkpointClosure) verdict = { ...verdict, checkpointClosure }
474
- const next = { ...host, conditions: [...preserved.values()], reviewSeen: host.reviewSeen || Boolean(origins), changedPaths: verdict.verdict === 'closed' ? [] : host.changedPaths }
481
+ if (verdict.verdict === 'closed' && active.postCiCorrection?.used) {
482
+ const p = active.postCiCorrection, bound = { evidenceDigest: p.evidence.approval.evidenceDigest, revision: host.snapshot.revision, nonce: host.snapshot.nonce, anchorSequence: p.anchorSequence, anchorEventDigest: p.anchorEventDigest }
483
+ const required = ['schema', ...Object.keys(bound), 'supported', 'evidence']
484
+ const proofs = host.reviewers.map(r => ({ reviewer: r.id, family: r.family, verification: receipt.reviews.find(v => v.reviewer === r.id && v.family === r.family)?.postCiEvidenceVerification }))
485
+ const valid = proofs.every(({ verification: v }) => dictionary(v, required) && v.schema === 'native-qe-post-ci-verification-1' && Object.keys(bound).every(k => v[k] === bound[k]) && v.supported === true && text(v.evidence))
486
+ verdict = valid ? { ...verdict, postCiEvidenceClosure: { ...bound, origins: proofs } } : result('qe', 'not-established', revision, ['native-post-ci-origin-verification-missing-or-stale'])
487
+ }
488
+ const next = { ...host, conditions: [...preserved.values()], reviewSeen: host.reviewSeen || Boolean(origins), changedPaths: verdict.verdict === 'closed' && !active.postCiCorrection?.used ? [] : host.changedPaths }
475
489
  return { host: next, snapshot: host.snapshot, origins: origins || [], receiptDigest: observed[1] === null ? null : digest(observed[1]), reason: origins ? null : verdict.reasons[0] || 'native-no-valid-origin', observationDigest, result: verdict, admission: false }
476
490
  }
477
491
  const bound = bindReviewers(host, reviewers, checkpoint)
478
492
  const snapshot = active?.snapshot.revision === revision ? active.snapshot : host?.snapshot.revision === revision ? host.snapshot : { nonce: randomUUID(), revision, manifest }
479
493
  const old = new Map((host?.snapshot.manifest || []).map(p => [p.path, p.digest])), current = new Map(manifest.map(p => [p.path, p.digest]))
480
- const changedPaths = host && host.snapshot.revision !== revision ? [...new Set([...old.keys(), ...current.keys()])].filter(p => old.get(p) !== current.get(p)).sort() : host?.changedPaths || []
494
+ const deltaPaths = host && host.snapshot.revision !== revision ? [...new Set([...old.keys(), ...current.keys()])].filter(p => old.get(p) !== current.get(p)).sort() : host?.changedPaths || []
495
+ const changedPaths = ['begin-continuation', 'begin-post-ci-correction'].includes(action) ? [...new Set([...(host?.changedPaths || []), ...deltaPaths])].sort() : deltaPaths
481
496
  const next = { schema: SCHEMA, phase: 'qe', reviewers: bound, snapshot, conditions: host?.conditions || [], reviewSeen: host?.reviewSeen || false, rework: Boolean(host && (host.rework || host.reviewSeen && host.snapshot.revision !== revision)), changedPaths }
482
- return { host: next, snapshot, origins: [], receiptDigest: null, reason: null, observationDigest, result: { ...result('qe', 'prepared', revision, []), nonce: snapshot.nonce, manifest, reviewers: bound, conditions: next.conditions, checkpointDigest: checkpoint?.checkpointDigest || null }, admission: !active }
497
+ return { host: next, snapshot, origins: [], receiptDigest: null, reason: null, observationDigest, result: { ...result('qe', 'prepared', revision, []), nonce: snapshot.nonce, manifest, reviewers: bound, conditions: next.conditions, checkpointDigest: active?.continuation ? active.checkpointDigest : checkpoint?.checkpointDigest || null }, admission: !active, ...(['begin-continuation', 'begin-post-ci-correction'].includes(action) ? { ownerApproval } : {}), ...(correction ? { correctionEvidence: correction } : {}) }
483
498
  })
484
499
  }
485
500
 
@@ -0,0 +1,22 @@
1
+ /** Raw-text trailing declaration grammar. Path authority is a separate layer. */
2
+ export function parseExpectedTargetBlock(text) {
3
+ const lines = String(text || '').split('\n');
4
+ const markers = [];
5
+ for (let i = 0; i < lines.length; i++) {
6
+ const match = /^EXPECTED_CODE_TARGETS:[ \t]*\r?$/.exec(lines[i]);
7
+ if (match && match[0] === lines[i]) markers.push(i);
8
+ }
9
+ const startLine = markers.length ? markers[0] : -1;
10
+ if (markers.length !== 1) return { ok: false, targets: [], startLine, reason: 'target-block-missing-or-duplicate' };
11
+ if (lines.slice(startLine).some(line => line.includes('\r'))) return { ok: false, targets: [], startLine, reason: 'target-block-malformed' };
12
+ const targets = [];
13
+ for (const line of lines.slice(startLine + 1)) {
14
+ if (!line.trim()) continue;
15
+ const match = /^- (.+)$/.exec(line);
16
+ // Full-line equality also refuses a final U+2028/U+2029 that $ can match before.
17
+ if (!match || match[0] !== line) return { ok: false, targets: [], startLine, reason: 'target-block-malformed' };
18
+ targets.push(match[1]);
19
+ }
20
+ if (!targets.length) return { ok: false, targets: [], startLine, reason: 'target-block-empty' };
21
+ return { ok: true, targets, startLine };
22
+ }
@@ -0,0 +1,213 @@
1
+ // Local measured Git/file provenance, never remote policy or reviewer authentication.
2
+ import { createHash } from 'node:crypto'
3
+ import { constants, closeSync, fstatSync, lstatSync, openSync, opendirSync, readSync } from 'node:fs'
4
+ import { resolve } from 'node:path'
5
+ import { execFileSync } from 'node:child_process'
6
+ import { performance } from 'node:perf_hooks'
7
+ const sha = s => createHash('sha256').update(s).digest('hex')
8
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
9
+ const fail = s => { throw Error('native-post-ci-' + s) }
10
+ function safe(repo, path) {
11
+ if (typeof path !== 'string' || path.startsWith('/') || /[\\\x00-\x1f]/.test(path) || path.split('/').some(p => !p || p === '.' || p === '..')) fail('path-invalid')
12
+ let file = repo
13
+ for (const part of path.split('/')) { file = resolve(file, part); if (lstatSync(file).isSymbolicLink()) fail('symlink') }
14
+ return file
15
+ }
16
+ function read(repo, proof, limit, selection = null, identity = null, imported = false, deadline = () => {}) {
17
+ const file = safe(repo, proof.path), st = lstatSync(file)
18
+ identity?.(proof.path, st); deadline()
19
+ if (!st.isFile() || st.size > limit || imported && (st.nlink !== 1 || st.size !== proof.size)) fail('file-type-or-limit')
20
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW), h = createHash('sha256')
21
+ const buffer = Buffer.alloc(65536), sample = selection ? Buffer.alloc(selection.length) : null
22
+ const chunks = limit <= 128 * 1024 ? [] : null
23
+ let count = 0, copied = 0
24
+ try {
25
+ const first = fstatSync(fd)
26
+ if (!first.isFile() || first.ino !== st.ino || first.dev !== st.dev || first.size !== st.size) fail('file-identity-changed')
27
+ while (true) {
28
+ deadline(); const n = readSync(fd, buffer, 0, buffer.length, null); if (!n) break
29
+ if (count + n > limit) fail('file-limit')
30
+ h.update(buffer.subarray(0, n)); if (chunks) chunks.push(Buffer.from(buffer.subarray(0, n)))
31
+ if (selection) { const start = Math.max(count, selection.offset), end = Math.min(count + n, selection.offset + selection.length); if (end > start) { buffer.copy(sample, start - selection.offset, start - count, end - count); copied += end - start } }
32
+ count += n
33
+ }
34
+ const end = fstatSync(fd), current = lstatSync(file), digest = h.digest('hex')
35
+ if (count !== st.size || end.size !== st.size || current.ino !== end.ino || current.dev !== end.dev || current.isSymbolicLink() || !current.isFile() || proof.digest && proof.digest !== digest || selection && copied !== selection.length) fail('file-short-changed-or-digest')
36
+ if (imported && (first.nlink !== 1 || end.nlink !== 1 || current.nlink !== 1 || end.dev !== st.dev || end.ino !== st.ino || current.size !== st.size || end.mtimeMs !== st.mtimeMs || current.mtimeMs !== st.mtimeMs || count !== proof.size)) fail('imported-identity-or-size-changed')
37
+ return { bytes: chunks ? Buffer.concat(chunks) : null, sample, digest, count }
38
+ } finally { closeSync(fd) }
39
+ }
40
+ // Only directories implied by immutable admitted paths may exist; Git ignore rules confer no exemption.
41
+ function inventory(repo, prefix, paths, imported = [], identity = null, deadline = () => {}) {
42
+ const files = new Set(paths), directories = new Map([[prefix.slice(0, -1), new Set()]])
43
+ if (files.size !== paths.length) fail('proof-overlap')
44
+ for (const r of imported) { files.add(r.path); const root = r.path.split('/').slice(0, 2).join('/'); if (!directories.has(root)) directories.set(root, new Set()) }
45
+ for (const path of files) {
46
+ const root = path.startsWith(prefix) ? prefix.slice(0, -1) : path.split('/').slice(0, 2).join('/')
47
+ const parts = path.slice(root.length + 1).split('/'); let dir = root
48
+ for (let i = 0; i < parts.length; i++) { directories.get(dir).add(parts[i]); if (i < parts.length - 1) { dir += '/' + parts[i]; if (!directories.has(dir)) directories.set(dir, new Set()) } }
49
+ }
50
+ if ([...directories.keys()].some(p => files.has(p))) fail('proof-file-directory-overlap')
51
+ if (identity && (files.size > 256 || directories.size > 256)) fail('immutable-inventory-limit')
52
+ const result = []
53
+ for (const [dir, expected] of directories) {
54
+ deadline(); const file = safe(repo, dir), before = lstatSync(file); if (!before.isDirectory()) fail('inventory-directory-type')
55
+ const entries = [], fd = opendirSync(file)
56
+ try { let entry; while ((entry = fd.readSync())) { if (entries.length >= 256 || !expected.has(entry.name)) fail('inventory-unlisted-or-limit'); entries.push(entry.name) } } finally { fd.closeSync() }
57
+ if (entries.length !== expected.size) fail('inventory-missing')
58
+ for (const path of [dir, ...entries.sort().map(name => dir + '/' + name)]) { const st = lstatSync(safe(repo, path)); deadline(); identity?.(path, st, !files.has(path)); if (path === dir && (st.ino !== before.ino || st.dev !== before.dev) || (files.has(path) ? !st.isFile() : !st.isDirectory())) fail('inventory-entry-type-or-identity'); result.push([path, st.dev, st.ino, st.size, st.mtimeMs]) }
59
+ }
60
+ return result
61
+ }
62
+ // Strict bounded parsers are exported for non-spawning adversarial framing controls.
63
+ export function parseImportedTree(bytes) {
64
+ if (bytes.length > 4194304 || bytes.length && bytes.at(-1) !== 0) fail('imported-tree-framing')
65
+ let text; try { text = new TextDecoder('utf8', { fatal: true }).decode(bytes) } catch { fail('imported-tree-encoding') }
66
+ const entries = text.split('\0').slice(0, -1), seen = new Set()
67
+ return entries.map(line => {
68
+ const m = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64}) +([0-9]+)\t([^\x00-\x1f]+)$/.exec(line)
69
+ if (!m || seen.has(m[4]) || !Number.isSafeInteger(Number(m[3])) || Number(m[3]) > 2097152) fail('imported-tree-entry')
70
+ seen.add(m[4]); return { path: m[4], mode: m[1], oid: m[2], size: Number(m[3]) }
71
+ })
72
+ }
73
+ export function parseImportedBatch(bytes, expected) {
74
+ if (bytes.length > 4194304) fail('imported-batch-limit')
75
+ const result = new Map(); let offset = 0
76
+ for (const row of expected) {
77
+ const newline = bytes.indexOf(10, offset)
78
+ if (newline < offset || newline - offset > 100 || result.has(row.oid) || !Number.isSafeInteger(row.size) || row.size < 0 || row.size > 2097152) fail('imported-batch-header')
79
+ const header = bytes.subarray(offset, newline).toString('ascii')
80
+ if (header !== row.oid + ' blob ' + row.size) fail('imported-batch-identity')
81
+ const end = newline + 1 + row.size
82
+ if (end >= bytes.length || bytes[end] !== 10) fail('imported-batch-short-or-delimiter')
83
+ result.set(row.oid, sha(bytes.subarray(newline + 1, end))); offset = end + 1
84
+ }
85
+ if (offset !== bytes.length) fail('imported-batch-trailing')
86
+ return result
87
+ }
88
+ export function importedIdentityRegistry() {
89
+ const files = new Map(), dirs = new Map()
90
+ return (path, st, directory = false) => { const registry = directory ? dirs : files, id = st.dev + ':' + st.ino, prior = registry.get(id); if (prior !== undefined && prior !== path) fail('imported-identity-alias'); registry.set(id, path) }
91
+ }
92
+ function observeImported(repo, rows, current, identity) {
93
+ const start = performance.now(), deadline = () => { if (performance.now() - start > 2000) fail('observation-deadline-exceeded') }
94
+ const sources = []; let total = 0
95
+ for (const row of current) {
96
+ deadline(); let st
97
+ try { st = lstatSync(safe(repo, row.path)) } catch (e) { if (row.digest === null && e.code === 'ENOENT') continue; throw e }
98
+ if (row.digest === null || !st.isFile() || st.size > 2097152) fail('current-source-type-size-or-absence')
99
+ identity(row.path, st); total += st.size; if (total > 16777216) fail('source-support-aggregate-limit')
100
+ sources.push([row.path, st.dev, st.ino, st.size, st.mtimeMs, st.nlink])
101
+ }
102
+ for (const row of rows) {
103
+ deadline(); const st = lstatSync(safe(repo, row.path)); total += st.size
104
+ if (total > 16777216) fail('source-support-aggregate-limit')
105
+ read(repo, row, 2097152, null, identity, true, deadline)
106
+ sources.push([row.path, st.dev, st.ino, st.size, st.mtimeMs, st.nlink])
107
+ }
108
+ return sources
109
+ }
110
+ export function measureNativePostCiEvidence({ repo, featurePath, evidencePath, previous, current, validate }) {
111
+ const prefix = featurePath + '/qa/post-ci-correction/'
112
+ if (!evidencePath?.startsWith(prefix) || typeof validate !== 'function') fail('descriptor-path-or-capability-invalid')
113
+ const descriptor = read(repo, { path: evidencePath }, 128 * 1024)
114
+ let evidence
115
+ try { evidence = JSON.parse(new TextDecoder('utf8', { fatal: true }).decode(descriptor.bytes)) } catch { fail('descriptor-malformed') }
116
+ if (!descriptor.bytes.equals(Buffer.from(JSON.stringify(evidence) + '\n'))) fail('descriptor-noncanonical')
117
+ validate(evidence, { featurePath, previous, current })
118
+ const q = evidence.qualification, s = evidence.scope, u = s.upstream, f = evidence.failure
119
+ const proofPaths = [evidencePath, ...new Set([f.log, q.color.log, q.neutral.log, q.ci.log].map(p => p.path)), s.fixture.patch.path, u.acceptanceEvidence.path, ...evidence.excludedEvidence.map(p => p.path)]
120
+ const importedRows = evidence.schema === 'native-qe-post-ci-evidence-2' ? s.importedArtifacts : [], identity = importedRows.length ? importedIdentityRegistry() : null
121
+ const supportObservation = identity ? observeImported(repo, importedRows, current, identity) : null
122
+ const membership = inventory(repo, prefix, proofPaths, importedRows, identity)
123
+ const witnesses = new Map(); let aggregate = 0
124
+ for (const p of new Map([f.log, q.color.log, q.neutral.log, q.ci.log].map(p => [p.path, p])).values()) {
125
+ const mode = [q.color, q.neutral].find(m => m.log.path === p.path)
126
+ const measured = read(repo, p, 16 * 1024 * 1024, mode?.witness)
127
+ aggregate += measured.count; if (aggregate > 64 * 1024 * 1024) fail('log-aggregate-limit')
128
+ if (mode) witnesses.set(p.path, measured.sample)
129
+ }
130
+ for (const p of [s.fixture.patch, u.acceptanceEvidence, ...evidence.excludedEvidence]) read(repo, p, p === s.fixture.patch ? 64 * 1024 : 128 * 1024)
131
+ validate(evidence, { featurePath, previous, current, witnesses })
132
+ // Diff's explicit stat-cache refresh can write even with optional locks disabled.
133
+ const git = (args, input) => execFileSync('git', ['-c', 'diff.autoRefreshIndex=false', ...args], { cwd: repo, input, timeout: 2000, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', GIT_NO_REPLACE_OBJECTS: '1' } })
134
+ const commit = (id, kind = 'commit') => { const got = git(['rev-parse', '--verify', id + '^{' + kind + '}']).toString().trim(); if (got !== id) fail('git-object-not-exact'); return got }
135
+ for (const id of [f.headCommit, q.sourceCommit, u.baseCommit, u.acceptedCommit, u.integrationCommit]) commit(id)
136
+ commit(u.integrationTree, 'tree')
137
+ const ancestry = git(['merge-base', u.baseCommit, u.acceptedCommit]).toString().trim()
138
+ if (ancestry !== u.baseCommit) fail('upstream-base-not-ancestor')
139
+ const info = git(['show', '-s', '--format=%P%n%T', u.integrationCommit]).toString().trim().split('\n'), parents = info[0].split(' ')
140
+ if (parents.length !== 2 || parents[1] !== u.acceptedCommit || info[1] !== u.integrationTree) fail('integration-parents-or-tree-invalid')
141
+ const preimport = parents[0]
142
+ // Missing blobs are established from bounded tree membership, never an ignored git error.
143
+ const totals = new Map()
144
+ const cache = new Map()
145
+ const blob = (tree, path) => {
146
+ const key = tree + ':' + path
147
+ if (cache.has(key)) return cache.get(key)
148
+ const entry = git(['ls-tree', '-z', tree, '--', path]).toString('utf8').split('\0').filter(Boolean)
149
+ if (!entry.length) { const absent = { kind: 'absent' }; cache.set(key, absent); return absent }
150
+ const match = /^100(?:644|755) blob ([a-f0-9]+)\t(.+)$/.exec(entry[0])
151
+ if (entry.length !== 1 || !match || match[2] !== path) fail('git-blob-type-invalid')
152
+ const bytes = git(['cat-file', 'blob', match[1]])
153
+ if (bytes.length > 2 * 1024 * 1024) fail('git-source-size-limit')
154
+ totals.set(tree, (totals.get(tree) || 0) + bytes.length); if (totals.get(tree) > 16 * 1024 * 1024) fail('git-source-aggregate-limit')
155
+ const state = { kind: 'bytes', digest: sha(bytes) }; cache.set(key, state); return state
156
+ }
157
+ const expected = r => r.digest === null ? { kind: 'absent' } : { kind: 'bytes', digest: r.digest }
158
+ for (const r of previous) {
159
+ if (!same(blob(f.headCommit, r.path), expected(r)) || !same(blob(preimport, r.path), expected(r))) fail('closed-source-mapping-invalid')
160
+ }
161
+ for (const r of current) {
162
+ if (!same(blob(q.sourceCommit, r.path), expected(r))) fail('qualified-source-mapping-invalid')
163
+ if (r.path !== s.fixture.path && !s.reviewArtifacts.some(a => a.path === r.path) && !same(blob(u.integrationTree, r.path), expected(r))) fail('integration-candidate-mapping-invalid')
164
+ }
165
+ for (const r of u.rows) {
166
+ // Manifest absence does not prove Git absence. An unenrolled file may already
167
+ // exist at the failed head; it must remain byte-identical until the import.
168
+ const prior = r.before.kind === 'absent' ? blob(f.headCommit, r.path) : r.before.kind === 'bytes' ? r.before : { kind: 'absent' }
169
+ if (r.before.kind === 'absent' && prior.kind === 'bytes' && (same(r.upstream, prior) || same(r.integrated, prior))) fail('import-not-changed')
170
+ if (!same(blob(preimport, r.path), prior) || !same(blob(u.acceptedCommit, r.path), r.upstream) || !same(blob(u.integrationTree, r.path), r.integrated)) fail('import-blob-mapping-invalid')
171
+ }
172
+ for (const r of s.workflowDependencies) {
173
+ if (![preimport, u.acceptedCommit, u.integrationTree, q.sourceCommit].every(t => same(blob(t, r.path), r.after))) fail('workflow-git-enrollment-invalid')
174
+ const actual = read(repo, { path: r.path, digest: r.after.digest }, 2 * 1024 * 1024)
175
+ if (actual.digest !== r.after.digest) fail('workflow-current-invalid')
176
+ }
177
+ if (importedRows.length) {
178
+ const roots = [...new Set(importedRows.map(r => r.path.split('/').slice(0, 2).join('/')))]
179
+ for (const tree of new Set([f.headCommit, u.baseCommit, preimport])) if (git(['ls-tree', '-z', tree, '--', ...roots]).length) fail('imported-root-preexists')
180
+ const trees = [u.acceptedCommit, u.integrationTree, q.sourceCommit], inventories = []
181
+ for (const tree of trees) {
182
+ const entries = parseImportedTree(git(['ls-tree', '-r', '-l', '-z', tree, '--', ...roots])).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
183
+ if (entries.length !== importedRows.length || entries.some((r, i) => r.path !== importedRows[i].path || r.size !== importedRows[i].size)) fail('imported-tree-membership-or-size')
184
+ for (const r of entries) { const key = tree + ':' + r.path; if (!cache.has(key)) { totals.set(tree, (totals.get(tree) || 0) + r.size); cache.set(key, { kind: 'bytes', digest: importedRows.find(p => p.path === r.path).digest }) } }
185
+ if ((totals.get(tree) || 0) > 16777216) fail('git-source-aggregate-limit')
186
+ inventories.push(entries)
187
+ }
188
+ if (!inventories.every(rows => same(rows, inventories[0]))) fail('imported-tree-bytes-changed')
189
+ const objects = [...new Map(inventories[0].map(r => [r.oid, r])).values()], hashes = new Map()
190
+ let batch = [], size = 0
191
+ const flush = () => { if (!batch.length) return; const parsed = parseImportedBatch(git(['cat-file', '--batch'], Buffer.from(batch.map(r => r.oid).join('\n') + '\n')), batch); for (const [oid, hash] of parsed) hashes.set(oid, hash); batch = []; size = 0 }
192
+ for (const row of objects) { const bytes = row.size + 102; if (size + bytes > 4194304) flush(); batch.push(row); size += bytes } flush()
193
+ if (inventories[0].some((r, i) => hashes.get(r.oid) !== importedRows[i].digest)) fail('imported-blob-digest')
194
+ }
195
+ const patch = git(['diff', '--no-ext-diff', '--no-textconv', '--binary', f.headCommit, q.sourceCommit, '--', s.fixture.path])
196
+ if (patch.length > 64 * 1024 || sha(patch) !== s.fixture.patch.digest) fail('fixture-patch-mismatch')
197
+ const exceptions = new Set(['base-ref', 'checkpoints.jsonl', 'native-qe-history.jsonl', 'native-qe-head.json', 'review-convergence-qe-host.json', 'review-convergence-qe-review.json', 'review-convergence-ideation-host.json', 'review-convergence-ideation-review.json'].map(p => featurePath + '/.fa-state/' + p).concat([featurePath + '/08_qe_report.md', featurePath + '/07_code_changes/report.md', featurePath + '/07_code_changes/change_manifest.md']))
198
+ const allowedExcluded = path => proofPaths.includes(path) || importedRows.some(r => r.path === path) || exceptions.has(path)
199
+ // Compare all source tree deltas too: a qualified manifest cannot hide unrelated edits.
200
+ const payload = new Set(current.map(r => r.path))
201
+ const qualifiedChanges = git(['diff', '--no-renames', '--name-only', '-z', preimport, q.sourceCommit, '--']).toString('utf8').split('\0').filter(Boolean)
202
+ if (qualifiedChanges.some(p => !payload.has(p) && !allowedExcluded(p))) fail('unqualified-commit-path')
203
+ for (const args of [['diff', '--no-renames', '--name-only', '-z', q.sourceCommit, '--'], ['ls-files', '--others', '--exclude-standard', '-z']]) {
204
+ const changed = git(args).toString('utf8').split('\0').filter(Boolean)
205
+ if (changed.some(p => !payload.has(p) && !allowedExcluded(p))) fail('unqualified-current-path')
206
+ }
207
+ const imported = git(['diff', '--no-renames', '--name-only', '-z', preimport, u.integrationCommit, '--']).toString('utf8').split('\0').filter(Boolean)
208
+ if (imported.some(p => !u.rows.some(r => r.path === p) && !s.reviewArtifacts.some(r => r.path === p) && !allowedExcluded(p))) fail('unaccounted-integration-path')
209
+ // Fixed Git facts are computed outside the journal lock; the ordinary index/HEAD/source fence rechecks current bytes.
210
+ if (identity && !same(supportObservation, observeImported(repo, importedRows, current, identity))) fail('imported-observation-changed')
211
+ if (!same(membership, inventory(repo, prefix, proofPaths, importedRows, identity))) fail('inventory-observation-changed')
212
+ return { evidencePath, evidence }
213
+ }