@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 +19 -11
- package/README.md +25 -1
- package/README.ru.md +19 -16
- package/package.json +1 -1
- package/sbom.json +30 -10
- package/templates/.claude/commands/analyst-manual.md +4 -2
- package/templates/.claude/skills/analyst-manual-full/SKILL.md +65 -37
- package/templates/.claude/skills/feature-adr/modules/06-implementation-plan.md +12 -1
- package/templates/.claude/skills/feature-adr/modules/07-code.md +11 -1
- package/templates/.claude/skills/feature-adr/modules/08-qe.md +20 -1
- package/templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs +8 -14
- package/templates/.claude/skills/feature-adr/scripts/check-review-convergence.mjs +42 -27
- package/templates/.claude/skills/feature-adr/scripts/expected-target-grammar.mjs +22 -0
- package/templates/.claude/skills/feature-adr/scripts/native-post-ci-evidence.mjs +213 -0
package/.dz-manifest.json
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"path": "README.md",
|
|
12
|
-
"sha256": "
|
|
12
|
+
"sha256": "a590413d707d91ebe9cf15a204bfce3988bccccb0727eca0cc1e47aaadaf2684"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "README.ru.md",
|
|
16
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
388
|
+
"sha256": "4eae99dfc266f9c32be3fd302b6a8316924461db8ae5038a169d7a0ec6398e89"
|
|
389
389
|
},
|
|
390
390
|
{
|
|
391
391
|
"path": "templates/.claude/skills/feature-adr/modules/07-code.md",
|
|
392
|
-
"sha256": "
|
|
392
|
+
"sha256": "7cd05898db005ad19fbaeb25af3d958d47946f5db195e6969009fd45953e3e30"
|
|
393
393
|
},
|
|
394
394
|
{
|
|
395
395
|
"path": "templates/.claude/skills/feature-adr/modules/08-qe.md",
|
|
396
|
-
"sha256": "
|
|
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": "
|
|
496
|
+
"sha256": "1c8f8e6ffad01ebdb1bcf6fce6cfb6baae6fe40a9ce377b3894f8addc06c8180"
|
|
497
497
|
},
|
|
498
498
|
{
|
|
499
499
|
"path": "templates/.claude/skills/feature-adr/scripts/check-review-convergence.mjs",
|
|
500
|
-
"sha256": "
|
|
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": "
|
|
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.
|
|
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 |
|
|
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
|
|
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 +
|
|
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 —
|
|
94
|
+
### 2. goap-research-ed25519 — исследование с проверяемыми ограничениями
|
|
95
95
|
|
|
96
|
-
**Что делает:** GOAP (Goal-Oriented Action Planning) + OODA loop
|
|
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) с
|
|
102
|
-
4.
|
|
103
|
-
5.
|
|
101
|
+
3. Выполняет OODA цикл (Observe → Orient → Decide → Act) с контрольными точками
|
|
102
|
+
4. Для каждого вывода указывает источник и степень подтверждения; подпись издателя проверяется только при наличии подписанной выдачи и закреплённого ключа
|
|
103
|
+
5. Формирует отчёт с отдельным статусом источников, подписей и цепочки цитирования
|
|
104
104
|
|
|
105
|
-
**3 уровня строгости:** moderate (0.85)
|
|
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** (
|
|
184
|
-
2.
|
|
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 +
|
|
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
|
-
-
|
|
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 |
|
|
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
|
|
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.
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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:
|
|
23
|
+
4. **Phase 2: Research** (goap-research-ed25519)
|
|
24
24
|
- Используй `scripts/goap_planner.py` и `scripts/ed25519_verifier.py`
|
|
25
|
-
-
|
|
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
|
-
|
|
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 (
|
|
55
|
+
│ Phase 2: RESEARCH (GOAP + статус доказательств) │
|
|
56
56
|
│ → Загрузи: ../goap-research-ed25519/SKILL.md │
|
|
57
|
-
│ → Output:
|
|
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
|
-
• "ок" — перейти к
|
|
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 |
|
|
124
|
-
|
|
125
|
-
| `moderate` | 0.85 | "ок" или "продолжай" |
|
|
126
|
-
| `strict` | 0.95 |
|
|
127
|
-
| `paranoid` | 0.99 |
|
|
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
|
-
|
|
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
|
-
##
|
|
148
|
+
## Research Findings
|
|
132
149
|
|
|
133
150
|
### Verification Status
|
|
134
151
|
- **Mode:** [selected_mode] ([threshold] threshold)
|
|
135
|
-
- **
|
|
136
|
-
- **
|
|
137
|
-
- **
|
|
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
|
-
###
|
|
140
|
-
[
|
|
158
|
+
### Findings
|
|
159
|
+
[Утверждение, ссылка, evidence class и его фактический trust class]
|
|
141
160
|
|
|
142
161
|
### Confidence Assessment
|
|
143
|
-
- **
|
|
144
|
-
- **Cross-Reference Verified (0.85-0.95):** [список]
|
|
162
|
+
- **Confidence:** [оценки с отдельным основанием; порог не заменяет подпись]
|
|
145
163
|
|
|
146
164
|
### Citation Chain
|
|
147
|
-
[
|
|
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:
|
|
177
|
+
⏸️ CHECKPOINT 2: Research Findings Ready
|
|
154
178
|
|
|
155
179
|
**Verification Status:**
|
|
156
|
-
- Mode: [
|
|
157
|
-
-
|
|
158
|
-
- Citation
|
|
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" — дополнительный
|
|
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
|
-
-
|
|
214
|
-
-
|
|
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
|
|
257
|
+
### Verification Status
|
|
232
258
|
- Research Mode: [mode]
|
|
233
|
-
-
|
|
234
|
-
- Citation
|
|
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
|
-
|
|
|
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
|
-
- [ ]
|
|
269
|
-
- [ ]
|
|
270
|
-
- [ ]
|
|
271
|
-
- [ ]
|
|
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` —
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
155
|
-
const
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
408
|
+
if (!targetBlock.ok) failures.push(`C3: EXPECTED_CODE_TARGETS declaration refused — ${targetBlock.reason}`);
|
|
412
409
|
else {
|
|
413
|
-
const
|
|
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(
|
|
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
|
|
154
|
-
if (block.
|
|
155
|
-
const planned = block
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|