@dzhechkov/keysarium 1.7.7 → 1.7.9
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 +21 -9
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/package.json +2 -2
- package/sbom.json +38 -8
- package/templates/.claude/skills/feature-adr/SKILL.md +79 -1
- package/templates/.claude/skills/feature-adr/modules/03.5-ideation-swarm.md +129 -0
- package/templates/.claude/skills/feature-adr/modules/06-implementation-plan.md +10 -0
- package/templates/.claude/skills/feature-adr/modules/07-code.md +29 -0
- package/templates/.claude/skills/feature-adr/modules/08-qe.md +162 -3
- package/templates/.claude/skills/feature-adr/scripts/build-coder-context.mjs +201 -0
- package/templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs +210 -28
- package/templates/.claude/skills/feature-adr/scripts/check-review-convergence.mjs +316 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Host checkpoint lineage, not a reviewer-supplied prior array, owns historical conditions.
|
|
3
|
+
// This checks consistency/freshness; it does not authenticate models or judge their evidence.
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
5
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { dirname, relative, resolve, sep } from 'node:path'
|
|
7
|
+
import { execFileSync } from 'node:child_process'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
export const SCHEMA = 'fa-review-convergence-1'
|
|
11
|
+
const phases = ['ideation', 'qe']
|
|
12
|
+
const severities = ['BLOCKER', 'CRITICAL', 'HIGH', 'WARNING', 'SUGGESTION']
|
|
13
|
+
const kinds = ['structural', 'wording']
|
|
14
|
+
const digest = value => createHash('sha256').update(value).digest('hex')
|
|
15
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
16
|
+
const text = value => typeof value === 'string' && value.trim().length > 0 && value.length <= 65536
|
|
17
|
+
const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)
|
|
18
|
+
const array = (value, limit = 512) => Array.isArray(value) && value.length <= limit
|
|
19
|
+
function dictionary(value, keys, required = keys) {
|
|
20
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).every(key => keys.includes(key)) && required.every(key => Object.hasOwn(value, key))
|
|
21
|
+
}
|
|
22
|
+
function strings(value, nonempty = false) { return array(value) && (!nonempty || value.length > 0) && value.every(text) && new Set(value).size === value.length }
|
|
23
|
+
function condition(c, owners) {
|
|
24
|
+
return dictionary(c, ['id', 'owner', 'severity', 'classification', 'requirement', 'scope', 'evidence']) && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/.test(c.id) && owners.includes(c.owner) && severities.includes(c.severity) && kinds.includes(c.classification) && text(c.requirement) && strings(c.scope, true) && text(c.evidence)
|
|
25
|
+
}
|
|
26
|
+
function risks(r, owners) { return dictionary(r, ['assessment', 'evidence', 'conditions']) && text(r.assessment) && text(r.evidence) && array(r.conditions) && r.conditions.every(c => condition(c, owners)) }
|
|
27
|
+
function checkpointProof(p) {
|
|
28
|
+
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))
|
|
29
|
+
}
|
|
30
|
+
function reviewerFamily(stage) { return stage.qeReviewerUsed === 'codex-fallback' ? 'codex' : stage.qeReviewerUsed }
|
|
31
|
+
function reviewerEntry(r, roster) {
|
|
32
|
+
const owners = roster.map(p => p.id)
|
|
33
|
+
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')
|
|
34
|
+
}
|
|
35
|
+
function result(phase, verdict, revision, reasons, unresolved = []) { return { schema: SCHEMA, phase, verdict, revision, unresolved, reasons } }
|
|
36
|
+
|
|
37
|
+
export function evaluateReviewConvergence(host, receipt, currentManifest) {
|
|
38
|
+
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')
|
|
41
|
+
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
|
+
if (!same(host.snapshot.manifest, currentManifest) || digest(JSON.stringify(currentManifest)) !== host.snapshot.revision) return refuse('artifact-manifest-changed')
|
|
45
|
+
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
|
+
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')
|
|
47
|
+
const all = new Map(host.conditions.map(c => [c.id, c]))
|
|
48
|
+
const verified = new Set()
|
|
49
|
+
const reasons = []
|
|
50
|
+
const needsAuthor = host.rework || (host.reviewSeen && host.conditions.length > 0)
|
|
51
|
+
if (needsAuthor && !receipt.author) return refuse('author-delta-missing')
|
|
52
|
+
if (receipt.author !== null) {
|
|
53
|
+
const a = receipt.author
|
|
54
|
+
if (!dictionary(a, ['addressed', 'changedScope', 'classification', 'delta', 'evidence', 'newRisks']) || !strings(a.addressed) || !strings(a.changedScope, true) || !kinds.includes(a.classification) || !text(a.delta) || !text(a.evidence) || !risks(a.newRisks, owners)) return refuse('author-delta-invalid')
|
|
55
|
+
if (host.changedPaths.some(path => !a.changedScope.includes(path))) return refuse('author-delta-omits-measured-changes')
|
|
56
|
+
if (a.addressed.some(id => !all.has(id))) return refuse('author-addressed-unknown-condition')
|
|
57
|
+
for (const c of a.newRisks.conditions) {
|
|
58
|
+
if (all.has(c.id) && !same(all.get(c.id), c)) return refuse('author-risk-condition-redefined')
|
|
59
|
+
all.set(c.id, c)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const r of receipt.reviews) {
|
|
63
|
+
if (!reviewerEntry(r, host.reviewers)) return refuse('reviewer-contract-invalid')
|
|
64
|
+
if (Object.hasOwn(r, 'checkpointVerification') && !checkpointProof(r.checkpointVerification)) return refuse('checkpoint-verification-contract-invalid')
|
|
65
|
+
if (receipt.author && !r.authorAssessmentChecked) return refuse('author-risk-assessment-not-reviewed')
|
|
66
|
+
if (receipt.author) {
|
|
67
|
+
const d = r.deltaVerification
|
|
68
|
+
if (!dictionary(d, ['classification', 'evidence', 'implementationVerified']) || !kinds.includes(d.classification) || !text(d.evidence) || typeof d.implementationVerified !== 'boolean') return refuse('independent-delta-classification-missing')
|
|
69
|
+
if ((receipt.author.classification === 'structural' || d.classification === 'structural') && (!d.implementationVerified || d.classification !== 'structural')) reasons.push('structural-delta-not-verified:' + r.reviewer)
|
|
70
|
+
} else if (r.deltaVerification !== null) return refuse('unexpected-delta-verification')
|
|
71
|
+
const own = r.conditions.concat(r.newRisks.conditions)
|
|
72
|
+
if (new Set(own.map(c => c.id)).size !== own.length) return refuse('duplicate-condition')
|
|
73
|
+
for (const c of host.conditions.filter(c => c.owner === r.reviewer)) if (!own.some(n => same(n, c))) return refuse('prior-condition-omitted-or-redefined:' + c.id)
|
|
74
|
+
for (const c of own) {
|
|
75
|
+
if (c.scope.some(path => !currentManifest.some(p => p.path === path))) return refuse('condition-scope-unmeasured:' + c.id)
|
|
76
|
+
if (all.has(c.id) && !same(all.get(c.id), c)) return refuse('condition-redefined:' + c.id)
|
|
77
|
+
all.set(c.id, c)
|
|
78
|
+
}
|
|
79
|
+
if (r.verdict === 'clean' && own.length > 0 && r.verifications.length === 0) return refuse('clean-verdict-contradicts-conditions')
|
|
80
|
+
if (r.verdict === 'conditional' && own.length === 0) return refuse('conditional-without-conditions')
|
|
81
|
+
if (r.verdict === 'no-go') reasons.push('reviewer-no-go:' + r.reviewer)
|
|
82
|
+
const seen = new Set()
|
|
83
|
+
for (const v of r.verifications) {
|
|
84
|
+
if (!dictionary(v, ['id', 'revision', 'classification', 'evidence', 'implementationVerified']) || seen.has(v.id) || !all.has(v.id) || all.get(v.id).owner !== r.reviewer || v.revision !== host.snapshot.revision || !kinds.includes(v.classification) || !text(v.evidence) || typeof v.implementationVerified !== 'boolean') return refuse('verification-invalid-or-foreign')
|
|
85
|
+
seen.add(v.id)
|
|
86
|
+
// A semantic change cannot become a weaker check by the author's label.
|
|
87
|
+
const structural = all.get(v.id).classification === 'structural' || receipt.author?.classification === 'structural' || v.classification === 'structural'
|
|
88
|
+
if (structural && (!v.implementationVerified || v.classification !== 'structural')) reasons.push('structural-verification-missing:' + v.id)
|
|
89
|
+
else verified.add(v.id)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const unresolved = [...all.keys()].filter(id => !verified.has(id))
|
|
93
|
+
for (const id of unresolved) reasons.push('unresolved:' + id)
|
|
94
|
+
if (receipt.author?.newRisks.conditions.some(c => !receipt.reviews.some(r => r.conditions.concat(r.newRisks.conditions).some(n => same(n, c))))) reasons.push('author-risk-not-adopted-by-reviewer')
|
|
95
|
+
return result(phase, reasons.length ? 'unresolved' : 'closed', host.snapshot.revision, reasons, unresolved)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
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')
|
|
100
|
+
let path = root
|
|
101
|
+
for (const part of rel.split('/')) {
|
|
102
|
+
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)
|
|
105
|
+
}
|
|
106
|
+
return path
|
|
107
|
+
}
|
|
108
|
+
function readJson(path) { return JSON.parse(readFileSync(path, 'utf8')) }
|
|
109
|
+
function writeJson(path, value) {
|
|
110
|
+
if (existsSync(path) && (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())) throw Error('unsafe-checkpoint')
|
|
111
|
+
const temporary = path + '.' + randomUUID() + '.tmp'
|
|
112
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { flag: 'wx' })
|
|
113
|
+
renameSync(temporary, path)
|
|
114
|
+
}
|
|
115
|
+
function pathsFor(repo, feature, phase) {
|
|
116
|
+
const prefix = relative(repo, feature).split(sep).join('/')
|
|
117
|
+
if (!prefix || prefix.startsWith('../')) throw Error('feature-must-be-inside-repo')
|
|
118
|
+
safePath(repo, prefix, false)
|
|
119
|
+
const targets = ['00_complexity_assessment.md', '01_requirements.md', '02_research.md', '04_domain_model.md', '05_architecture.md']
|
|
120
|
+
if (phase === 'qe') targets.push('06_implementation_plan.md', '03.5_ideation_report.md')
|
|
121
|
+
const adr = safePath(repo, prefix + '/03_adr')
|
|
122
|
+
if (existsSync(adr) && !lstatSync(adr).isDirectory()) throw Error('adr-not-directory')
|
|
123
|
+
const decisions = existsSync(adr) ? readdirSync(adr).filter(p => /^[0-9]{3}-.+\.md$/.test(p)).sort() : []
|
|
124
|
+
if (phase === 'ideation' && !decisions.length) throw Error('adr-missing')
|
|
125
|
+
const paths = targets.filter(p => existsSync(resolve(feature, p))).concat(decisions.map(p => '03_adr/' + p)).map(p => prefix + '/' + p)
|
|
126
|
+
for (const required of phase === 'ideation' ? ['01_requirements.md', '05_architecture.md'] : ['01_requirements.md', '06_implementation_plan.md']) if (!paths.includes(prefix + '/' + required)) throw Error('design-input-missing:' + required)
|
|
127
|
+
if (phase === 'qe') {
|
|
128
|
+
const plan = readFileSync(safePath(repo, prefix + '/06_implementation_plan.md', false), 'utf8')
|
|
129
|
+
const block = plan.split(/^EXPECTED_CODE_TARGETS:\s*$/m)
|
|
130
|
+
if (block.length !== 2) throw Error('target-block-missing-or-duplicate')
|
|
131
|
+
const planned = block[1].split('\n').filter(line => line.trim()).map(line => {
|
|
132
|
+
const match = /^- (.+)$/.exec(line)
|
|
133
|
+
if (!match) throw Error('target-block-malformed')
|
|
134
|
+
return match[1]
|
|
135
|
+
})
|
|
136
|
+
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
|
+
const baseFile = resolve(feature, '.fa-state/base-ref')
|
|
141
|
+
const base = existsSync(baseFile) ? readFileSync(baseFile, 'utf8').trim() : 'HEAD'
|
|
142
|
+
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']]) {
|
|
144
|
+
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'))
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return [...new Set(paths)].sort()
|
|
149
|
+
}
|
|
150
|
+
export function measureManifest(repo, feature, phase) {
|
|
151
|
+
return pathsFor(repo, feature, phase).map(path => {
|
|
152
|
+
const absolute = safePath(repo, path)
|
|
153
|
+
if (!existsSync(absolute)) return { path, digest: null }
|
|
154
|
+
if (!lstatSync(absolute).isFile()) throw Error('non-file-artifact:' + path)
|
|
155
|
+
return { path, digest: digest(readFileSync(absolute)) }
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
function checkpointPaths(repo, feature, phase) {
|
|
159
|
+
const stateRel = relative(repo, feature).split(sep).join('/') + '/.fa-state'
|
|
160
|
+
const state = safePath(repo, stateRel)
|
|
161
|
+
mkdirSync(state, { recursive: true })
|
|
162
|
+
return { host: safePath(repo, stateRel + '/review-convergence-' + phase + '-host.json'), review: safePath(repo, stateRel + '/review-convergence-' + phase + '-review.json') }
|
|
163
|
+
}
|
|
164
|
+
function qeCheckpoint(repo, feature) {
|
|
165
|
+
const rel = relative(repo, feature).split(sep).join('/') + '/.fa-state/checkpoints.jsonl'
|
|
166
|
+
const file = safePath(repo, rel)
|
|
167
|
+
if (!existsSync(file)) return null
|
|
168
|
+
let checkpoint = null
|
|
169
|
+
for (const line of readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
|
|
170
|
+
const item = JSON.parse(line)
|
|
171
|
+
if (item.stage === 'qe') checkpoint = item.result
|
|
172
|
+
}
|
|
173
|
+
return checkpoint ? { checkpointDigest: digest(JSON.stringify(checkpoint)), checkpoint } : null
|
|
174
|
+
}
|
|
175
|
+
function seriousFindings(source) {
|
|
176
|
+
return (array(source?.gaps) ? source.gaps : []).filter(gap => [gap.sev, gap.severity, gap.priority].some(v => /(?:^|[^A-Z0-9])(BLOCKER|CRITICAL|HIGH|P0|P1)(?:$|[^A-Z0-9])/.test(String(v || '').toUpperCase())))
|
|
177
|
+
}
|
|
178
|
+
function reconcileCheckpoint(host, receipt, measured, repo, feature) {
|
|
179
|
+
if (!measured) return null
|
|
180
|
+
const stage = measured.checkpoint
|
|
181
|
+
for (const [base, source, family, missing] of [
|
|
182
|
+
['qe-primary', stage.qe, reviewerFamily(stage), !stage.qe || Boolean(stage.qe?.convergenceUnavailable)],
|
|
183
|
+
['qe-precision', stage.qe2, 'claude', host.reviewers.some(r => r.id.startsWith('qe-precision')) && (!stage.qe2 || !stage.qe2.reportWritten)],
|
|
184
|
+
]) {
|
|
185
|
+
const findings = seriousFindings(source)
|
|
186
|
+
if (!missing && !findings.length) continue
|
|
187
|
+
const origin = host.reviewers.find(r => (r.id === base || r.id === base + ':' + family) && r.family === family)
|
|
188
|
+
const review = receipt.reviews.find(r => r.reviewer === origin?.id)
|
|
189
|
+
const proof = review?.checkpointVerification
|
|
190
|
+
if (!origin || !checkpointProof(proof) || proof.checkpointDigest !== measured.checkpointDigest || !text(proof.evidence) || proof.supported !== true || !['native', 'mode-b', 'fallback'].includes(proof.route) || !array(proof.findings) || proof.findings.length !== findings.length || !(proof.reportDigest === null || hash(proof.reportDigest))) return null
|
|
191
|
+
const mapped = new Set()
|
|
192
|
+
for (const gap of findings) {
|
|
193
|
+
const item = proof.findings.find(v => v.digest === digest(JSON.stringify(gap)))
|
|
194
|
+
if (!dictionary(item, ['digest', 'conditionId']) || mapped.has(item.conditionId) || !host.conditions.some(c => c.id === item.conditionId && c.owner === origin.id && ['BLOCKER', 'CRITICAL', 'HIGH'].includes(c.severity)) || !review.verifications.some(v => v.id === item.conditionId && v.revision === host.snapshot.revision && v.implementationVerified === true && v.classification === 'structural')) return null
|
|
195
|
+
mapped.add(item.conditionId)
|
|
196
|
+
}
|
|
197
|
+
if (base === 'qe-precision' && missing) {
|
|
198
|
+
const file = safePath(repo, relative(repo, feature).split(sep).join('/') + '/08_qe_report.md', false)
|
|
199
|
+
const report = readFileSync(file)
|
|
200
|
+
if (digest(report) !== proof.reportDigest || !/## Primary QE pass/.test(report.toString()) || !/## Precision QE pass/.test(report.toString()) || !/Combined Step-8 grade:/.test(report.toString())) return null
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { ...measured, revision: host.snapshot.revision }
|
|
204
|
+
}
|
|
205
|
+
function retainPendingReceipt(host, path) {
|
|
206
|
+
if (!host || !existsSync(path)) return host
|
|
207
|
+
let receipt
|
|
208
|
+
try { receipt = readJson(path) } catch (_) { throw Error('pending-reviewer-receipt-needs-origin-review') }
|
|
209
|
+
if (!dictionary(receipt, ['schema', 'phase', 'nonce', 'revision', 'reviews', 'author']) || receipt.schema !== SCHEMA || receipt.phase !== host.phase || !text(receipt.nonce) || !hash(receipt.revision) || !array(receipt.reviews, 16) || !receipt.reviews.length || new Set(receipt.reviews.map(r => r?.reviewer)).size !== receipt.reviews.length) throw Error('pending-reviewer-receipt-needs-origin-review')
|
|
210
|
+
const owners = host.reviewers.map(r => r.id), preserved = new Map(host.conditions.map(c => [c.id, c]))
|
|
211
|
+
const fresh = receipt.nonce === host.snapshot.nonce && receipt.revision === host.snapshot.revision
|
|
212
|
+
let problem = false, witnessed = false
|
|
213
|
+
for (const r of receipt.reviews) {
|
|
214
|
+
if (!reviewerEntry(r, host.reviewers) || Object.hasOwn(r, 'checkpointVerification') && !checkpointProof(r.checkpointVerification)) { problem = true; continue }
|
|
215
|
+
const own = r.conditions.concat(r.newRisks.conditions)
|
|
216
|
+
if (new Set(own.map(c => c.id)).size !== own.length) { problem = true; continue }
|
|
217
|
+
witnessed = true
|
|
218
|
+
for (const c of own) {
|
|
219
|
+
// Stale evidence may preserve a known condition; it cannot originate new host authority.
|
|
220
|
+
if (preserved.has(c.id)) { if (!same(preserved.get(c.id), c)) problem = true }
|
|
221
|
+
else if (!fresh || c.scope.some(path => !host.snapshot.manifest.some(p => p.path === path))) problem = true
|
|
222
|
+
else if (condition(c, owners) && c.owner === r.reviewer) preserved.set(c.id, c)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const next = { ...host, conditions: [...preserved.values()], reviewSeen: host.reviewSeen || witnessed }
|
|
226
|
+
// This imports original independent findings only, never closure, author claims or a grade.
|
|
227
|
+
return { next, problem }
|
|
228
|
+
}
|
|
229
|
+
function bindReviewers(host, requested, measured) {
|
|
230
|
+
if (!host) return requested
|
|
231
|
+
const bound = host.reviewers.map(r => ({ ...r }))
|
|
232
|
+
for (const actual of requested) {
|
|
233
|
+
const prior = bound.find(r => r.id === actual.id)
|
|
234
|
+
if (!prior) throw Error('host-lineage-conflict')
|
|
235
|
+
if (prior.family === actual.family) continue
|
|
236
|
+
const originated = host.reviewSeen || host.conditions.some(c => c.owner === prior.id) || measured && (prior.id === 'qe-primary' && reviewerFamily(measured.checkpoint) === prior.family || prior.id === 'qe-precision' && measured.checkpoint.qe2)
|
|
237
|
+
if (!originated) prior.family = actual.family
|
|
238
|
+
else if (!bound.some(r => r.id === actual.id + ':' + actual.family)) bound.push({ id: actual.id + ':' + actual.family, family: actual.family })
|
|
239
|
+
}
|
|
240
|
+
if (host.reviewers.some(r => !r.id.includes(':') && !requested.some(n => n.id === r.id)) || bound.length > 16) throw Error('host-lineage-conflict')
|
|
241
|
+
return bound
|
|
242
|
+
}
|
|
243
|
+
export function runGate({ action, repo, feature, phase, reviewers }) {
|
|
244
|
+
if (!phases.includes(phase) || !['prepare', 'evaluate'].includes(action)) throw Error('invalid-command')
|
|
245
|
+
repo = realpathSync(repo); feature = resolve(repo, feature)
|
|
246
|
+
const current = measureManifest(repo, feature, phase)
|
|
247
|
+
const paths = checkpointPaths(repo, feature, phase)
|
|
248
|
+
let host = existsSync(paths.host) ? readJson(paths.host) : null
|
|
249
|
+
const measuredCheckpoint = phase === 'qe' ? qeCheckpoint(repo, feature) : null
|
|
250
|
+
if (action === 'prepare') {
|
|
251
|
+
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')
|
|
257
|
+
}
|
|
258
|
+
reviewers = bindReviewers(host, reviewers, measuredCheckpoint)
|
|
259
|
+
const revision = digest(JSON.stringify(current))
|
|
260
|
+
const snapshot = host?.snapshot?.revision === revision ? host.snapshot : { nonce: randomUUID(), revision, manifest: current }
|
|
261
|
+
const priorManifest = new Map((host?.snapshot?.manifest || []).map(p => [p.path, p.digest]))
|
|
262
|
+
const newManifest = new Map(current.map(p => [p.path, p.digest]))
|
|
263
|
+
const changedPaths = host?.snapshot?.revision === revision ? host.changedPaths : host ? [...new Set([...(host.changedPaths || []), ...[...new Set([...priorManifest.keys(), ...newManifest.keys()])].filter(path => priorManifest.get(path) !== newManifest.get(path))])].sort() : []
|
|
264
|
+
const next = { schema: SCHEMA, phase, reviewers, snapshot, conditions: host?.conditions || [], reviewSeen: host?.reviewSeen || false, rework: Boolean(host && (host.rework || host.reviewSeen && (host.snapshot.revision !== revision || host.conditions.length > 0))), changedPaths }
|
|
265
|
+
writeJson(paths.host, next)
|
|
266
|
+
return { ...result(phase, 'prepared', revision, []), nonce: snapshot.nonce, manifest: current, conditions: next.conditions, reviewers, checkpointDigest: measuredCheckpoint?.checkpointDigest || null }
|
|
267
|
+
}
|
|
268
|
+
if (!host) return result(phase, 'not-established', null, ['host-lineage-missing'])
|
|
269
|
+
if (!existsSync(paths.review)) return result(phase, 'not-established', host.snapshot.revision, ['reviewer-receipt-missing'], host.conditions.map(c => c.id))
|
|
270
|
+
const receipt = readJson(paths.review)
|
|
271
|
+
let verdict = evaluateReviewConvergence(host, receipt, current)
|
|
272
|
+
let closure = null
|
|
273
|
+
if (phase === 'qe' && verdict.verdict === 'closed' && measuredCheckpoint) {
|
|
274
|
+
closure = reconcileCheckpoint(host, receipt, measuredCheckpoint, repo, feature)
|
|
275
|
+
if (!closure) verdict = result(phase, 'not-established', host.snapshot.revision, ['historical-checkpoint-own-verification-missing'])
|
|
276
|
+
}
|
|
277
|
+
// Preserve independently owned findings even on refusal. Never import author/scribe claims.
|
|
278
|
+
// Invalid ownership/meaning refuses without overwriting the prior host authority.
|
|
279
|
+
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
|
+
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)
|
|
290
|
+
}
|
|
291
|
+
writeJson(paths.host, { ...host, conditions: [...preserved.values()], reviewSeen: true, changedPaths: verdict.verdict === 'closed' ? [] : host.changedPaths })
|
|
292
|
+
if (closure && verdict.verdict === 'closed') return { ...verdict, checkpointClosure: closure }
|
|
293
|
+
return verdict
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
297
|
+
let phase = 'qe'
|
|
298
|
+
try {
|
|
299
|
+
const args = process.argv.slice(2)
|
|
300
|
+
const action = args.shift()
|
|
301
|
+
const options = {}
|
|
302
|
+
while (args.length) {
|
|
303
|
+
const key = args.shift()
|
|
304
|
+
if (!['--repo', '--feature', '--phase', '--reviewers'].includes(key) || !args.length || Object.hasOwn(options, key)) throw Error('invalid-arguments')
|
|
305
|
+
options[key] = args.shift()
|
|
306
|
+
}
|
|
307
|
+
phase = options['--phase']
|
|
308
|
+
if (!options['--repo'] || !options['--feature'] || !phases.includes(phase)) throw Error('invalid-arguments')
|
|
309
|
+
const output = runGate({ action, repo: options['--repo'], feature: options['--feature'], phase, reviewers: options['--reviewers'] ? JSON.parse(options['--reviewers']) : undefined })
|
|
310
|
+
process.stdout.write(JSON.stringify(output) + '\n')
|
|
311
|
+
process.exitCode = ['closed', 'prepared'].includes(output.verdict) ? 0 : output.verdict === 'unresolved' ? 1 : 3
|
|
312
|
+
} catch (error) {
|
|
313
|
+
process.stdout.write(JSON.stringify(result(phases.includes(phase) ? phase : 'qe', 'not-established', null, [String(error.message)])) + '\n')
|
|
314
|
+
process.exitCode = 3
|
|
315
|
+
}
|
|
316
|
+
}
|