@dzhechkov/keysarium 1.7.13 → 1.7.14
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 +16 -8
- package/README.md +19 -1
- package/package.json +1 -1
- package/sbom.json +27 -7
- 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,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"path": "README.md",
|
|
12
|
-
"sha256": "
|
|
12
|
+
"sha256": "8e3d0f7279b85203b4c7a35ae6375f016e607276066e6c73396ac94e58b399cb"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "README.ru.md",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"path": "package.json",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "52ff221d4ce111588f31f3940e4374e5ca76eb868753e28560e4ba2b5487d768"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "src/cli.js",
|
|
@@ -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": "mcdM1pWxiIzC5bkl/QD4KSoJh1L8lBnQLn7KwGZL2ueH3vAvNjOnuef0d02eVjB86skGdmuapJcb5jqZHX/mCw=="
|
|
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.14`. <!-- dz:version -->
|
|
9
9
|
|
|
10
10
|
Site: https://aicoding.space · Source: https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/keysarium
|
|
11
11
|
|
|
@@ -562,3 +562,21 @@ terminal `RC_EXIT:0` witness; failure refuses coding/cache access. Admission pre
|
|
|
562
562
|
and existing `.fa-state/base-ref` (HEAD only when absent); actual coder initialization never resets
|
|
563
563
|
an existing base. Plain uses the same executable helper and mandatory instructions, without an
|
|
564
564
|
automatic dispatcher. Admission observes current bytes; final QE remains the freshness check.
|
|
565
|
+
|
|
566
|
+
### Literal implementation-plan targets
|
|
567
|
+
|
|
568
|
+
Feature-ADR K2 completeness, native admission and core plan authority share a raw-text trailing
|
|
569
|
+
`EXPECTED_CODE_TARGETS:` grammar: one uppercase column-zero marker with optional ASCII padding,
|
|
570
|
+
LF-only declaration, nonempty `- ` path rows and trim()-empty blanks through EOF. Suffix headings,
|
|
571
|
+
prose, alternate bullets, CR/CRLF or duplicate raw markers refuse the whole block; markers inside
|
|
572
|
+
examples/comments also count. Payloads stay literal, including backticks and whitespace. Existing
|
|
573
|
+
path authority and native I/O/history bounds still apply. Core establishes no valid subset from
|
|
574
|
+
malformed or rejected/rewritten plan paths. Explicit args retain their separate normalization and
|
|
575
|
+
precedence; coder text remains diagnostic. The standalone grammar helper ships beside the skill's
|
|
576
|
+
native checker and K2 script. No Markdown authenticity is inferred.
|
|
577
|
+
|
|
578
|
+
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.
|
|
579
|
+
|
|
580
|
+
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.
|
|
581
|
+
|
|
582
|
+
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/keysarium",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.14",
|
|
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": "8e3d0f7279b85203b4c7a35ae6375f016e607276066e6c73396ac94e58b399cb"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"name": "dz:canonical-json-sha256-v2",
|
|
62
|
-
"value": "
|
|
62
|
+
"value": "52ff221d4ce111588f31f3940e4374e5ca76eb868753e28560e4ba2b5487d768"
|
|
63
63
|
}
|
|
64
64
|
]
|
|
65
65
|
},
|
|
@@ -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",
|
|
@@ -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
|
+
}
|