@dzhechkov/keysarium 1.7.9 → 1.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.dz-manifest.json CHANGED
@@ -9,7 +9,7 @@
9
9
  },
10
10
  {
11
11
  "path": "README.md",
12
- "sha256": "13e7e27322403ce45af5be7df1fa565178b5df73ffeea1b0bbe6b3faddce9f45"
12
+ "sha256": "10e8e4f470823f26d20a8cb4b5680dde97fd9654f9545b81de099405bd2dba32"
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": "96bcc52bfe2180fd9976589895f1f328f6c8ac62e24d22042d1105a5c16f4148"
24
+ "sha256": "fec2ebcb8a572ee95f1ac5b41264933d853a01d96b85a826e208a7bf4b49da1b"
25
25
  },
26
26
  {
27
27
  "path": "src/cli.js",
@@ -493,7 +493,7 @@
493
493
  },
494
494
  {
495
495
  "path": "templates/.claude/skills/feature-adr/scripts/check-review-convergence.mjs",
496
- "sha256": "80f9691b8e94bf310461aeaaaa23af6e336d41d0962fdf137737639655cbdeb8"
496
+ "sha256": "09c74f1441d9c42603fbb84402e6826cf96535d209be9802e0e4b2fea01dac7d"
497
497
  },
498
498
  {
499
499
  "path": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
@@ -1001,5 +1001,5 @@
1001
1001
  }
1002
1002
  ]
1003
1003
  },
1004
- "signature": "n1PCRDF3YW7Tcdp+QdGzlK+p7PQGfraCKUO8yQxu6zrvXx6WUALgVsvmfAwh+QXmPZzINT6kYnKmzijtcEkrCw=="
1004
+ "signature": "wfYnIS4MAw7T3m282RmpXCeIUVVAi+TDfNI4DmRaHJEy3h/gpJdtlv1TMTRz6dVfMA0fOcxZrJKWBXO+JFEcCg=="
1005
1005
  }
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @dzhechkov/keysarium
2
2
 
3
- Current package version: `1.7.9`. <!-- dz:version -->
3
+ Current package version: `1.7.10`. <!-- dz:version -->
4
4
 
5
5
  Site: https://aicoding.space · Source: https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/keysarium
6
6
 
@@ -526,3 +526,7 @@ Confirmation, scope, family and budget gates still apply. No automatic repair lo
526
526
  Repeated prepare retains all pending paths until verified closure. Actual fallback families bind before dispatch while prior owners remain required. Same-slug QE repair uses current own-reviewer evidence bound to the complete historical checkpoint; original findings/grades remain visible, and failed Confirmation still blocks delivery.
527
527
 
528
528
  Before fallback, host prepare retains independently written pending receipt findings under the original owner even if that reviewer returned null. Unreconciled pending evidence pauses dispatch for originating-reviewer repair; it cannot be replaced by a clean fallback to obtain closure.
529
+
530
+ After a reviewed artifact is renamed or removed, convergence keeps its previously host-bound condition scope in the measured manifest; an absent old path has a null digest. Current discovered paths remain measured. Both prepare and evaluate validate the retained snapshot and reject unbound or unsafe scope paths. The original condition and originating reviewer verification remain required; a rename alone does not close a finding.
531
+
532
+ QE also measures the exact `.dz/guard.json` policy configuration when it is in the current Git index or selected review base. This includes newly staged and intent-to-add files; base-tracked deletions retain a null digest. Both declared targets and discovered changes use this rule. Untracked files absent from that base do not qualify through older history. Other `.dz/` runtime files remain excluded, and existing path, symlink, freshness and reviewer-ownership checks still apply.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/keysarium",
3
- "version": "1.7.9",
3
+ "version": "1.7.10",
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": "13e7e27322403ce45af5be7df1fa565178b5df73ffeea1b0bbe6b3faddce9f45"
28
+ "content": "10e8e4f470823f26d20a8cb4b5680dde97fd9654f9545b81de099405bd2dba32"
29
29
  }
30
30
  ]
31
31
  },
@@ -59,7 +59,7 @@
59
59
  },
60
60
  {
61
61
  "name": "dz:canonical-json-sha256-v2",
62
- "value": "96bcc52bfe2180fd9976589895f1f328f6c8ac62e24d22042d1105a5c16f4148"
62
+ "value": "fec2ebcb8a572ee95f1ac5b41264933d853a01d96b85a826e208a7bf4b49da1b"
63
63
  }
64
64
  ]
65
65
  },
@@ -1239,7 +1239,7 @@
1239
1239
  "hashes": [
1240
1240
  {
1241
1241
  "alg": "SHA-256",
1242
- "content": "80f9691b8e94bf310461aeaaaa23af6e336d41d0962fdf137737639655cbdeb8"
1242
+ "content": "09c74f1441d9c42603fbb84402e6826cf96535d209be9802e0e4b2fea01dac7d"
1243
1243
  }
1244
1244
  ]
1245
1245
  },
@@ -34,13 +34,25 @@ function reviewerEntry(r, roster) {
34
34
  }
35
35
  function result(phase, verdict, revision, reasons, unresolved = []) { return { schema: SCHEMA, phase, verdict, revision, unresolved, reasons } }
36
36
 
37
+ function artifactPath(rel) {
38
+ return text(rel) && !rel.startsWith('/') && !rel.includes('\\') && !rel.split('/').some(p => !p || p === '.' || p === '..') && !/[\x00-\x1f]/.test(rel)
39
+ }
40
+ function hostProblem(host) {
41
+ if (!dictionary(host, ['schema', 'phase', 'reviewers', 'snapshot', 'conditions', 'reviewSeen', 'rework', 'changedPaths']) || host.schema !== SCHEMA || !phases.includes(host.phase) || !array(host.reviewers, 16) || host.reviewers.length === 0) return 'host-lineage-invalid'
42
+ const owners = host.reviewers.map(r => r?.id)
43
+ if (new Set(owners).size !== owners.length || !host.reviewers.every(r => dictionary(r, ['id', 'family']) && text(r.id) && ['codex', 'claude', 'owner-exception'].includes(r.family))) return 'host-reviewers-invalid'
44
+ if (!dictionary(host.snapshot, ['nonce', 'revision', 'manifest']) || !text(host.snapshot.nonce) || !hash(host.snapshot.revision) || !array(host.conditions) || !host.conditions.every(c => condition(c, owners)) || new Set(host.conditions.map(c => c.id)).size !== host.conditions.length || typeof host.reviewSeen !== 'boolean' || typeof host.rework !== 'boolean' || !strings(host.changedPaths) || !host.changedPaths.every(artifactPath)) return 'host-snapshot-invalid'
45
+ const manifest = host.snapshot.manifest
46
+ if (!Array.isArray(manifest) || !manifest.every(p => dictionary(p, ['path', 'digest']) && artifactPath(p.path) && (p.digest === null || hash(p.digest))) || new Set(manifest.map(p => p.path)).size !== manifest.length || digest(JSON.stringify(manifest)) !== host.snapshot.revision) return 'host-manifest-invalid'
47
+ if (host.conditions.some(c => c.scope.some(path => !manifest.some(p => p.path === path)))) return 'host-condition-scope-unbound'
48
+ return null
49
+ }
37
50
  export function evaluateReviewConvergence(host, receipt, currentManifest) {
38
51
  const phase = host?.phase
39
- const refuse = reason => result(phase || 'qe', 'not-established', host?.snapshot?.revision || null, [reason], (host?.conditions || []).map(c => c.id))
40
- if (!dictionary(host, ['schema', 'phase', 'reviewers', 'snapshot', 'conditions', 'reviewSeen', 'rework', 'changedPaths']) || host.schema !== SCHEMA || !phases.includes(phase) || !array(host.reviewers, 16) || host.reviewers.length === 0) return refuse('host-lineage-invalid')
52
+ const refuse = reason => result(phase || 'qe', 'not-established', host?.snapshot?.revision || null, [reason], (array(host?.conditions) ? host.conditions : []).map(c => c?.id))
53
+ const problem = hostProblem(host)
54
+ if (problem) return refuse(problem)
41
55
  const owners = host.reviewers.map(r => r.id)
42
- if (new Set(owners).size !== owners.length || !host.reviewers.every(r => dictionary(r, ['id', 'family']) && text(r.id) && ['codex', 'claude', 'owner-exception'].includes(r.family))) return refuse('host-reviewers-invalid')
43
- if (!dictionary(host.snapshot, ['nonce', 'revision', 'manifest']) || !text(host.snapshot.nonce) || !hash(host.snapshot.revision) || !array(host.conditions) || !host.conditions.every(c => condition(c, owners)) || typeof host.reviewSeen !== 'boolean' || typeof host.rework !== 'boolean' || !strings(host.changedPaths)) return refuse('host-snapshot-invalid')
44
56
  if (!same(host.snapshot.manifest, currentManifest) || digest(JSON.stringify(currentManifest)) !== host.snapshot.revision) return refuse('artifact-manifest-changed')
45
57
  if (!dictionary(receipt, ['schema', 'phase', 'nonce', 'revision', 'reviews', 'author']) || receipt.schema !== SCHEMA || receipt.phase !== phase || receipt.nonce !== host.snapshot.nonce || receipt.revision !== host.snapshot.revision) return refuse('receipt-phase-or-revision-invalid')
46
58
  if (!array(receipt.reviews, 16) || receipt.reviews.length !== owners.length || new Set(receipt.reviews.map(r => r?.reviewer)).size !== owners.length) return refuse('reviewer-set-invalid')
@@ -96,12 +108,17 @@ export function evaluateReviewConvergence(host, receipt, currentManifest) {
96
108
  }
97
109
 
98
110
  function safePath(root, rel, allowMissing = true) {
99
- if (!text(rel) || rel.startsWith('/') || rel.includes('\\') || rel.split('/').some(p => !p || p === '.' || p === '..') || /[\x00-\x1f]/.test(rel)) throw Error('unsafe-artifact-path')
111
+ if (!artifactPath(rel)) throw Error('unsafe-artifact-path')
100
112
  let path = root
101
113
  for (const part of rel.split('/')) {
102
114
  path = resolve(path, part)
103
- if (!existsSync(path)) { if (allowMissing) return resolve(root, rel); throw Error('artifact-missing:' + rel) }
104
- if (lstatSync(path).isSymbolicLink()) throw Error('symlink-artifact:' + rel)
115
+ let entry
116
+ try { entry = lstatSync(path) } catch (error) {
117
+ if (error.code !== 'ENOENT') throw error
118
+ if (allowMissing) return resolve(root, rel)
119
+ throw Error('artifact-missing:' + rel)
120
+ }
121
+ if (entry.isSymbolicLink()) throw Error('symlink-artifact:' + rel)
105
122
  }
106
123
  return path
107
124
  }
@@ -134,21 +151,30 @@ function pathsFor(repo, feature, phase) {
134
151
  return match[1]
135
152
  })
136
153
  if (!planned.length) throw Error('target-block-empty')
137
- if (planned.some(p => /^(features|\.dz|\.agentic-qe|roam)\//.test(p))) throw Error('circular-review-target')
138
- paths.push(...planned)
139
- // Additions/deletions are discovered by the host, not the author delta request.
140
154
  const baseFile = resolve(feature, '.fa-state/base-ref')
141
155
  const base = existsSync(baseFile) ? readFileSync(baseFile, 'utf8').trim() : 'HEAD'
142
156
  if (!/^[A-Za-z0-9_./:-]+$/.test(base) || base.startsWith('-')) throw Error('base-ref-invalid')
143
- for (const command of [['diff', '--name-only', '-z', base, '--'], ['ls-files', '--others', '--exclude-standard', '-z']]) {
157
+ const guard = '.dz/guard.json'
158
+ // Exact index membership includes staged additions/intent-to-add. The selected
159
+ // base proves deletions; descendants and unrelated history confer no authority.
160
+ const proof = [['ls-files', '--cached', '-z', '--', guard], ['ls-tree', '-r', '--name-only', '-z', base, '--', guard]].map(command =>
161
+ execFileSync('git', command, { cwd: repo, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 }).split('\0').includes(guard))
162
+ const allowedGuard = p => p === guard && proof.some(Boolean)
163
+ if (planned.some(p => /^(features|\.dz|\.agentic-qe|roam)\//.test(p) && !allowedGuard(p))) throw Error('circular-review-target')
164
+ paths.push(...planned)
165
+ // Additions/deletions are discovered by the host, not the author delta request.
166
+ for (const command of [['diff', '--no-renames', '--name-only', '-z', base, '--'], ['ls-files', '--others', '--exclude-standard', '-z']]) {
144
167
  const names = execFileSync('git', command, { cwd: repo, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 }).split('\0').filter(Boolean)
145
- paths.push(...names.filter(p => !p.startsWith('features/') && !p.startsWith('.dz/') && !p.startsWith('.agentic-qe/') && !p.startsWith('roam/') && p !== 'architecture/map.json'))
168
+ paths.push(...names.filter(p => allowedGuard(p) || !p.startsWith('features/') && !p.startsWith('.dz/') && !p.startsWith('.agentic-qe/') && !p.startsWith('roam/') && p !== 'architecture/map.json'))
146
169
  }
147
170
  }
148
171
  return [...new Set(paths)].sort()
149
172
  }
150
173
  export function measureManifest(repo, feature, phase) {
151
- return pathsFor(repo, feature, phase).map(path => {
174
+ return measurePaths(repo, pathsFor(repo, feature, phase))
175
+ }
176
+ function measurePaths(repo, paths) {
177
+ return [...new Set(paths)].sort().map(path => {
152
178
  const absolute = safePath(repo, path)
153
179
  if (!existsSync(absolute)) return { path, digest: null }
154
180
  if (!lstatSync(absolute).isFile()) throw Error('non-file-artifact:' + path)
@@ -243,17 +269,27 @@ function bindReviewers(host, requested, measured) {
243
269
  export function runGate({ action, repo, feature, phase, reviewers }) {
244
270
  if (!phases.includes(phase) || !['prepare', 'evaluate'].includes(action)) throw Error('invalid-command')
245
271
  repo = realpathSync(repo); feature = resolve(repo, feature)
246
- const current = measureManifest(repo, feature, phase)
247
272
  const paths = checkpointPaths(repo, feature, phase)
248
- let host = existsSync(paths.host) ? readJson(paths.host) : null
273
+ const hostExists = existsSync(paths.host)
274
+ let host = hostExists ? readJson(paths.host) : null
275
+ if (hostExists) {
276
+ const problem = hostProblem(host)
277
+ if (problem) throw Error(problem)
278
+ if (host.phase !== phase) throw Error('host-lineage-conflict')
279
+ }
280
+ let pending = null
281
+ if (host && existsSync(paths.review)) {
282
+ try { pending = retainPendingReceipt(host, paths.review) } catch (error) { if (action === 'prepare') throw error }
283
+ }
284
+ const retained = pending?.next || host
285
+ // Only scopes bound by the validated prior snapshot can extend current discovery.
286
+ const current = measurePaths(repo, pathsFor(repo, feature, phase).concat((retained?.conditions || []).flatMap(c => c.scope)))
249
287
  const measuredCheckpoint = phase === 'qe' ? qeCheckpoint(repo, feature) : null
250
288
  if (action === 'prepare') {
251
289
  if (!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')
252
- if (host && (host.phase !== phase || host.schema !== SCHEMA || !array(host.conditions) || typeof host.reviewSeen !== 'boolean')) throw Error('host-lineage-conflict')
253
- if (host && existsSync(paths.review)) {
254
- const pending = retainPendingReceipt(host, paths.review)
255
- host = pending.next; writeJson(paths.host, host)
256
- if (pending.problem) throw Error('pending-reviewer-receipt-needs-origin-review')
290
+ if (pending) {
291
+ host = pending.next
292
+ if (pending.problem) { writeJson(paths.host, host); throw Error('pending-reviewer-receipt-needs-origin-review') }
257
293
  }
258
294
  reviewers = bindReviewers(host, reviewers, measuredCheckpoint)
259
295
  const revision = digest(JSON.stringify(current))
@@ -274,21 +310,14 @@ export function runGate({ action, repo, feature, phase, reviewers }) {
274
310
  closure = reconcileCheckpoint(host, receipt, measuredCheckpoint, repo, feature)
275
311
  if (!closure) verdict = result(phase, 'not-established', host.snapshot.revision, ['historical-checkpoint-own-verification-missing'])
276
312
  }
277
- // Preserve independently owned findings even on refusal. Never import author/scribe claims.
278
- // Invalid ownership/meaning refuses without overwriting the prior host authority.
313
+ // Preserve only fresh, independently owned findings bound by the prior snapshot.
314
+ // Stale or malformed receipts cannot originate measurement authority on refusal.
315
+ const preserved = new Map((pending?.next.conditions || host.conditions).map(c => [c.id, c]))
279
316
  const owners = host.reviewers.map(r => r.id)
280
- const preserved = new Map(host.conditions.map(c => [c.id, c]))
281
- if (receipt?.schema === SCHEMA && receipt.phase === phase && array(receipt.reviews)) {
282
- for (const r of receipt.reviews) if (owners.includes(r?.reviewer) && host.reviewers.find(p => p.id === r.reviewer)?.family === r.family && r.independent === true) {
283
- for (const c of [...(array(r.conditions) ? r.conditions : []), ...(array(r.newRisks?.conditions) ? r.newRisks.conditions : [])]) {
284
- if (condition(c, owners) && c.owner === r.reviewer && !preserved.has(c.id)) preserved.set(c.id, c)
285
- }
286
- }
287
- }
288
317
  if (receipt?.phase === phase && receipt.nonce === host.snapshot.nonce && receipt.revision === host.snapshot.revision && risks(receipt.author?.newRisks, owners)) {
289
- for (const c of receipt.author.newRisks.conditions) if (!preserved.has(c.id)) preserved.set(c.id, c)
318
+ for (const c of receipt.author.newRisks.conditions) if (!preserved.has(c.id) && c.scope.every(path => host.snapshot.manifest.some(p => p.path === path))) preserved.set(c.id, c)
290
319
  }
291
- writeJson(paths.host, { ...host, conditions: [...preserved.values()], reviewSeen: true, changedPaths: verdict.verdict === 'closed' ? [] : host.changedPaths })
320
+ writeJson(paths.host, { ...host, conditions: [...preserved.values()], reviewSeen: pending?.next.reviewSeen || host.reviewSeen, changedPaths: verdict.verdict === 'closed' ? [] : host.changedPaths })
292
321
  if (closure && verdict.verdict === 'closed') return { ...verdict, checkpointClosure: closure }
293
322
  return verdict
294
323
  }