Skip to content

Commit 74c5785

Browse files
committed
feat(host-parity): enterprise residual ledger and cannotClaim upgrade gate
Add UnalignedLedgerV1 fixtures, ledger/upgrade libs, MIN_CANNOT_CLAIM floor, and host-parity ledger tests. Document U-* residual path and keep validate off the host-parity suite (optimization-controls remains the quality chain).
1 parent 2473416 commit 74c5785

12 files changed

Lines changed: 594 additions & 10 deletions

‎changelogs/unreleased.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
## 当前未发布实现候选
77

8+
- **HostParity 企业级持续对齐(E1/E3/E5/E7)**:新增 `UnalignedLedgerV1` fixture(U-A1~U-C3)+ `parity-unaligned-ledger` / `parity-upgrade-decision`;`MIN_CANNOT_CLAIM` + floor 断言;`test:host-parity` 串联 `test-host-parity-ledger`;站点/AGENTS 投影补台账与 9 步 checklist;validate 默认不挂 host-parity(以 optimization-controls 为准)。需求:`Grok-Codex未对齐收敛与持续对齐保证` CP3。
89
- **CLI 默认五宿主(A2)**:bare `devcodex init` / `update` 经 `cmdInit({ includeExtended: true })` 部署 copilot+claude+codex+gemini+grok(含 Grok `installed-plugins` digest 同步);**不**绑 `cmdInitHost('all')`(避免 bare init 触发 `HOST_INSTRUCTION_COLLISION`)。`--host copilot` 显式 `includeExtended: false`。`test-cli-command-registry` 断言第二参 opts。help/README/host-instruction-projection 文案同步。需求:`Grok插件随update自动同步` CP1 A2 / CP2 v0.3。
910
- **C16 TTFV + WorkspaceRootScanBan(PI-20260724-01)**:GrokTurnChecklist 增加 `scan-hygiene` / `ttfv-first-delivery`;Hook `neverApprove` 拦截 monorepo/workspace 根递归 inventory(绝对根路径、`dir /s`、cwd=根时的相对 `-Recurse`/`-Depth`);SC16 与 `17-compliance`/`01-common`/website 消费者对齐;探针与 Hook 一级子路径一致;`test:host-parity` 纳入回归矩阵。
1011
- **意图驱动五宿主能力映射**:新增 active `host-capability-routing`、`CapabilityIntentDecisionV1`、`HostLeverCatalogV1`、`OriginalInstructionRefV1`、8-row local catalog 与纯校验器;薄 Rule 保留原指令权威、portable-first、CP/Auto/S01~S07 边界,5 个逻辑宿主/8 个 variant 在证据未知、过期、重复或 MCP absent 时 fail closed。当前 canonical native eligible=0,Phase 1 不新增 MCP primitive;README/website 明确收益、catalog/证据/消费者维护代价与 MCP Tool 升级阈值。

‎host-projections/AGENTS.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@
7878
## Passive-hook 宿主(Grok)
7979

8080
- Grok 等 passive-hook 宿主:优先 `devcodex grok` Full 入口;禁止把 full-capable 解读为 UserPromptSubmit 已注入 PC0。
81-
- GrokTurnChecklist(可扫):PC0~PC7 → Intent→Skill bundle(非 chat:intent+compliance+user-visible-output-contract+工作流+report+memory)→ 实质任务 → report/memory/台账;不得因无 inject/省 token 省略 S05/S07/C17。
81+
- GrokTurnChecklist(可扫,权威列表见 `host-parity-scorecard` 共 9 步):PC0~PC7 → Intent→Skill bundle(非 chat:intent+compliance+user-visible-output-contract+工作流+report+memory)→ context-plan → **scan-hygiene(C16 禁 workspace 根 Recurse inventory)** → **ttfv-first-delivery(同轮首交付)** → work/gates → report/memory/台账 → honest-ceiling;不得因无 inject/省 token 省略 S05/S07/C17。
82+
- HostParity 残差台账(机器权威):`scripts/fixtures/host-parity/unaligned-ledger.v1.json`(U-A1~U-C3);cannotClaim 缩减须 ParityUpgradeDecision。
8283

8384
## 按需扩展与故障回退
8485

‎package.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"test:discipline-execution": "node scripts/test-discipline-execution-probe.js",
7070
"test:host-instruction-projection": "node scripts/generate-host-instruction-projections.js --check && node scripts/test-host-instruction-projection.js",
7171
"test:host-adapters": "node scripts/test-host-adapters.js",
72-
"test:host-parity": "node scripts/test-host-parity-scorecard.js && node scripts/test-workspace-root-scan-ban.js && node scripts/test-host-parity-remaining.js",
72+
"test:host-parity": "node scripts/test-host-parity-scorecard.js && node scripts/test-workspace-root-scan-ban.js && node scripts/test-host-parity-remaining.js && node scripts/test-host-parity-ledger.js",
7373
"test:host-installation": "node scripts/test-host-installation.js",
7474
"benchmark:execution-chain": "node scripts/benchmark-execution-chain.js",
7575
"benchmark:runtime-indexes": "node scripts/benchmark-runtime-indexes.js",
@@ -223,6 +223,9 @@
223223
"scripts/lib/cli-install-commands.js",
224224
"scripts/lib/cli-maintenance-commands.js",
225225
"scripts/lib/host-parity-scorecard.js",
226+
"scripts/lib/parity-unaligned-ledger.js",
227+
"scripts/lib/parity-upgrade-decision.js",
228+
"scripts/fixtures/host-parity/",
226229
"scripts/lib/cli-command-registry.js",
227230
"scripts/lib/cli-json-contract.js",
228231
"scripts/lib/cli-observability-commands.js",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "devcodex://host-parity/unaligned-ledger.v1.schema.json",
4+
"title": "UnalignedLedgerV1",
5+
"type": "object",
6+
"additionalProperties": false,
7+
"required": ["schemaVersion", "updatedAt", "items"],
8+
"properties": {
9+
"schemaVersion": { "const": "UnalignedLedgerV1" },
10+
"updatedAt": { "type": "string", "minLength": 1 },
11+
"items": {
12+
"type": "array",
13+
"minItems": 11,
14+
"maxItems": 11,
15+
"items": {
16+
"type": "object",
17+
"additionalProperties": false,
18+
"required": ["id", "title", "status", "closePath", "owner", "evidenceRefs"],
19+
"properties": {
20+
"id": {
21+
"type": "string",
22+
"enum": [
23+
"U-A1", "U-A2", "U-A3", "U-A4",
24+
"U-B1", "U-B2", "U-B3", "U-B4",
25+
"U-C1", "U-C2", "U-C3"
26+
]
27+
},
28+
"title": { "type": "string", "minLength": 1 },
29+
"status": { "type": "string", "enum": ["open", "closed", "wont-fix"] },
30+
"closePath": { "type": "string", "minLength": 1 },
31+
"owner": { "type": "string", "minLength": 1 },
32+
"evidenceRefs": {
33+
"type": "array",
34+
"items": {
35+
"type": "object",
36+
"additionalProperties": false,
37+
"required": ["kind", "ref"],
38+
"properties": {
39+
"kind": { "type": "string", "minLength": 1 },
40+
"ref": { "type": "string", "minLength": 1 },
41+
"note": { "type": "string" }
42+
}
43+
}
44+
},
45+
"closedAt": { "type": ["string", "null"] },
46+
"notes": { "type": "string" }
47+
}
48+
}
49+
}
50+
}
51+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
{
2+
"schemaVersion": "UnalignedLedgerV1",
3+
"updatedAt": "2026-07-24T00:00:00.000Z",
4+
"items": [
5+
{
6+
"id": "U-A1",
7+
"title": "UserPromptSubmit context injection",
8+
"status": "open",
9+
"closePath": "platform P-GROK-1",
10+
"owner": "xai-platform",
11+
"evidenceRefs": [],
12+
"notes": "Passive stdout ignored on Grok"
13+
},
14+
{
15+
"id": "U-A2",
16+
"title": "UserPromptSubmit optional block",
17+
"status": "open",
18+
"closePath": "platform P-GROK-2",
19+
"owner": "xai-platform",
20+
"evidenceRefs": [],
21+
"notes": "eventSupportsHardBlock(grok, UPS)=false"
22+
},
23+
{
24+
"id": "U-A3",
25+
"title": "Stop assistant final text / verified PC0",
26+
"status": "open",
27+
"closePath": "platform P-GROK-3",
28+
"owner": "xai-platform",
29+
"evidenceRefs": [],
30+
"notes": "Stop payload often lacks assistant body"
31+
},
32+
{
33+
"id": "U-A4",
34+
"title": "Stop hard-block incomplete closure",
35+
"status": "open",
36+
"closePath": "platform P-GROK-4",
37+
"owner": "xai-platform",
38+
"evidenceRefs": [],
39+
"notes": "eventSupportsHardBlock(grok, Stop)=false"
40+
},
41+
{
42+
"id": "U-B1",
43+
"title": "Hook mode multi-event vs PreTool-only",
44+
"status": "open",
45+
"closePath": "platform + honest matrix",
46+
"owner": "devcodex",
47+
"evidenceRefs": [],
48+
"notes": "doctor mode=hook partial"
49+
},
50+
{
51+
"id": "U-B2",
52+
"title": "Bootstrap injection path",
53+
"status": "open",
54+
"closePath": "U-A1 + S07 model duty",
55+
"owner": "devcodex",
56+
"evidenceRefs": [],
57+
"notes": "kernelInjected must stay false on Grok bridge"
58+
},
59+
{
60+
"id": "U-B3",
61+
"title": "Auto whitelist runtime weaker than Codex",
62+
"status": "open",
63+
"closePath": "docs honesty; no false guarantee",
64+
"owner": "devcodex",
65+
"evidenceRefs": []
66+
},
67+
{
68+
"id": "U-B4",
69+
"title": "Passive events strip hard-block (intentional)",
70+
"status": "open",
71+
"closePath": "keep + regression probes",
72+
"owner": "devcodex",
73+
"evidenceRefs": [],
74+
"notes": "wont-fix for platform parity; keep as intentional residual"
75+
},
76+
{
77+
"id": "U-C1",
78+
"title": "Full entry requires devcodex grok; plain child Partial",
79+
"status": "open",
80+
"closePath": "entry docs + recommendedEntry",
81+
"owner": "devcodex",
82+
"evidenceRefs": []
83+
},
84+
{
85+
"id": "U-C2",
86+
"title": "full-capable != inject",
87+
"status": "open",
88+
"closePath": "PC5 / cannotClaim copy",
89+
"owner": "devcodex",
90+
"evidenceRefs": []
91+
},
92+
{
93+
"id": "U-C3",
94+
"title": "GrokTurnChecklist model duty heavier",
95+
"status": "open",
96+
"closePath": "checklist + omission probes",
97+
"owner": "devcodex",
98+
"evidenceRefs": []
99+
}
100+
]
101+
}

‎scripts/lib/host-parity-scorecard.js‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,31 @@ const GROK_INTENT_SKILL_BUNDLES = Object.freeze({
3838
other: Object.freeze(['intent', 'compliance', 'user-visible-output-contract', 'plan', 'report', 'memory'])
3939
})
4040

41+
/** Minimum cannotClaim strings that must remain unless ParityUpgradeDecision allows shrink (E3). */
42+
const MIN_CANNOT_CLAIM = Object.freeze([
43+
'UserPromptSubmit context injection (passive stdout ignored on Grok)',
44+
'Stop hard-block of incomplete turns',
45+
'verified-present PC0 without assistant payload on Stop',
46+
'Grok === Codex hook-enforced bootstrap'
47+
])
48+
49+
/**
50+
* @param {string[]} cannotClaim
51+
* @returns {{ ok: true } | never}
52+
*/
53+
function assertCannotClaimFloor(cannotClaim) {
54+
const list = Array.isArray(cannotClaim) ? cannotClaim.map(String) : []
55+
for (const required of MIN_CANNOT_CLAIM) {
56+
if (!list.includes(required)) {
57+
throw new Error(`CANNOT_CLAIM_FLOOR_VIOLATION: missing required claim: ${required}`)
58+
}
59+
}
60+
if (list.length < MIN_CANNOT_CLAIM.length) {
61+
throw new Error(`CANNOT_CLAIM_FLOOR_VIOLATION: length ${list.length} < ${MIN_CANNOT_CLAIM.length}`)
62+
}
63+
return { ok: true }
64+
}
65+
4166
const CHECK_REPAIR_CATALOG = Object.freeze({
4267
kernelAgentsMd: {
4368
check: 'kernelAgentsMd',
@@ -356,12 +381,8 @@ function evaluateGrokHostParity(input = {}) {
356381
? 'devcodex grok # Full evidence: --rules binds workspace AGENTS.md'
357382
: (repairSteps[0] && repairSteps[0].command) || 'devcodex update --host grok && devcodex grok'
358383

359-
const cannotClaim = [
360-
'UserPromptSubmit context injection (passive stdout ignored on Grok)',
361-
'Stop hard-block of incomplete turns',
362-
'verified-present PC0 without assistant payload on Stop',
363-
'Grok === Codex hook-enforced bootstrap'
364-
]
384+
const cannotClaim = [...MIN_CANNOT_CLAIM]
385+
assertCannotClaimFloor(cannotClaim)
365386

366387
const failedChecks = Object.entries(checks).filter(([, ok]) => !ok).map(([k]) => k)
367388
const repairPreview = repairSteps
@@ -453,6 +474,8 @@ module.exports = {
453474
GROK_TURN_EXECUTION_CHECKLIST,
454475
GROK_INTENT_SKILL_BUNDLES,
455476
CHECK_REPAIR_CATALOG,
477+
MIN_CANNOT_CLAIM,
478+
assertCannotClaimFloor,
456479
evaluateGrokHostParity,
457480
composeEntryCheckBlock,
458481
entryCheckAssistSuffix,
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use strict'
2+
3+
/**
4+
* UnalignedLedgerV1 — machine residual ledger for Grok vs Codex HostParity (E1/E8).
5+
* Authority: scripts/fixtures/host-parity/unaligned-ledger.v1.json
6+
*/
7+
8+
const fs = require('fs')
9+
const path = require('path')
10+
11+
const SCHEMA_VERSION = 'UnalignedLedgerV1'
12+
const REQUIRED_IDS = Object.freeze([
13+
'U-A1', 'U-A2', 'U-A3', 'U-A4',
14+
'U-B1', 'U-B2', 'U-B3', 'U-B4',
15+
'U-C1', 'U-C2', 'U-C3'
16+
])
17+
18+
const DEFAULT_LEDGER_PATH = path.join(__dirname, '../fixtures/host-parity/unaligned-ledger.v1.json')
19+
const DEFAULT_SCHEMA_PATH = path.join(__dirname, '../fixtures/host-parity/unaligned-ledger.schema.json')
20+
21+
/**
22+
* @param {string} [ledgerPath]
23+
* @returns {object}
24+
*/
25+
function loadUnalignedLedger(ledgerPath = DEFAULT_LEDGER_PATH) {
26+
const raw = fs.readFileSync(ledgerPath, 'utf8')
27+
const ledger = JSON.parse(raw)
28+
return validateUnalignedLedger(ledger)
29+
}
30+
31+
/**
32+
* Lightweight structural validation (no ajv dependency).
33+
* @param {object} ledger
34+
* @returns {object} same ledger if valid
35+
*/
36+
function validateUnalignedLedger(ledger) {
37+
if (!ledger || typeof ledger !== 'object' || Array.isArray(ledger)) {
38+
throw new Error('UNALIGNED_LEDGER_INVALID: root must be object')
39+
}
40+
if (ledger.schemaVersion !== SCHEMA_VERSION) {
41+
throw new Error(`UNALIGNED_LEDGER_INVALID: schemaVersion must be ${SCHEMA_VERSION}`)
42+
}
43+
if (typeof ledger.updatedAt !== 'string' || !ledger.updatedAt) {
44+
throw new Error('UNALIGNED_LEDGER_INVALID: updatedAt required')
45+
}
46+
if (!Array.isArray(ledger.items) || ledger.items.length !== REQUIRED_IDS.length) {
47+
throw new Error(`UNALIGNED_LEDGER_INVALID: items must have exactly ${REQUIRED_IDS.length} entries`)
48+
}
49+
const seen = new Set()
50+
for (const item of ledger.items) {
51+
if (!item || typeof item !== 'object') {
52+
throw new Error('UNALIGNED_LEDGER_INVALID: item must be object')
53+
}
54+
if (!REQUIRED_IDS.includes(item.id)) {
55+
throw new Error(`UNALIGNED_LEDGER_INVALID: unknown id ${item.id}`)
56+
}
57+
if (seen.has(item.id)) {
58+
throw new Error(`UNALIGNED_LEDGER_INVALID: duplicate id ${item.id}`)
59+
}
60+
seen.add(item.id)
61+
if (!item.title || typeof item.title !== 'string') {
62+
throw new Error(`UNALIGNED_LEDGER_INVALID: ${item.id} title required`)
63+
}
64+
if (!['open', 'closed', 'wont-fix'].includes(item.status)) {
65+
throw new Error(`UNALIGNED_LEDGER_INVALID: ${item.id} bad status`)
66+
}
67+
if (!item.closePath || !item.owner) {
68+
throw new Error(`UNALIGNED_LEDGER_INVALID: ${item.id} closePath/owner required`)
69+
}
70+
if (!Array.isArray(item.evidenceRefs)) {
71+
throw new Error(`UNALIGNED_LEDGER_INVALID: ${item.id} evidenceRefs must be array`)
72+
}
73+
if (item.status === 'closed' && item.evidenceRefs.length < 1) {
74+
throw new Error(`UNALIGNED_LEDGER_CLOSE_WITHOUT_EVIDENCE: ${item.id}`)
75+
}
76+
for (const ref of item.evidenceRefs) {
77+
if (!ref || typeof ref.kind !== 'string' || typeof ref.ref !== 'string') {
78+
throw new Error(`UNALIGNED_LEDGER_INVALID: ${item.id} evidenceRefs entries need kind+ref`)
79+
}
80+
}
81+
}
82+
for (const id of REQUIRED_IDS) {
83+
if (!seen.has(id)) {
84+
throw new Error(`UNALIGNED_LEDGER_INVALID: missing id ${id}`)
85+
}
86+
}
87+
return ledger
88+
}
89+
90+
/**
91+
* Attempt to mark an item closed; fails without evidence.
92+
* @param {object} ledger
93+
* @param {string} id
94+
* @param {{ kind: string, ref: string, note?: string }[]} evidenceRefs
95+
* @param {string} [closedAt]
96+
* @returns {object} new ledger (does not write disk)
97+
*/
98+
function closeUnalignedItem(ledger, id, evidenceRefs, closedAt = new Date().toISOString()) {
99+
const next = JSON.parse(JSON.stringify(ledger))
100+
const item = next.items.find((x) => x.id === id)
101+
if (!item) throw new Error(`UNALIGNED_LEDGER_UNKNOWN_ID: ${id}`)
102+
if (!Array.isArray(evidenceRefs) || evidenceRefs.length < 1) {
103+
throw new Error(`UNALIGNED_LEDGER_CLOSE_WITHOUT_EVIDENCE: ${id}`)
104+
}
105+
item.status = 'closed'
106+
item.evidenceRefs = evidenceRefs
107+
item.closedAt = closedAt
108+
next.updatedAt = closedAt
109+
return validateUnalignedLedger(next)
110+
}
111+
112+
/**
113+
* Markdown projection for humans (requirement-side copy).
114+
* @param {object} ledger
115+
* @returns {string}
116+
*/
117+
function formatUnalignedLedgerMarkdown(ledger) {
118+
validateUnalignedLedger(ledger)
119+
const lines = [
120+
'# UnalignedLedgerV1 (projection)',
121+
'',
122+
`> schemaVersion: ${ledger.schemaVersion} · updatedAt: ${ledger.updatedAt}`,
123+
'> **Authority**: `scripts/fixtures/host-parity/unaligned-ledger.v1.json`',
124+
'',
125+
'| ID | Title | Status | Close path | Owner |',
126+
'|----|-------|:------:|------------|-------|'
127+
]
128+
for (const item of ledger.items) {
129+
lines.push(`| ${item.id} | ${item.title} | ${item.status} | ${item.closePath} | ${item.owner} |`)
130+
}
131+
lines.push('')
132+
return lines.join('\n')
133+
}
134+
135+
module.exports = {
136+
SCHEMA_VERSION,
137+
REQUIRED_IDS,
138+
DEFAULT_LEDGER_PATH,
139+
DEFAULT_SCHEMA_PATH,
140+
loadUnalignedLedger,
141+
validateUnalignedLedger,
142+
closeUnalignedItem,
143+
formatUnalignedLedgerMarkdown
144+
}

0 commit comments

Comments
 (0)