Skip to content

Commit 1a7ee5a

Browse files
committed
feat(visible-output): ArtifactPathColumnGate (PF-175)
Require a portable path column on user-facing delivery items, keep Rich semantic links without redundant absolute-path lines, and sync 02-output-paths, always-on common instructions, delivery checklist, report skill, and website report-output-flow. Add classifier tests.
1 parent eb1511b commit 1a7ee5a

10 files changed

Lines changed: 145 additions & 34 deletions

File tree

changelogs/unreleased.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
66
## 当前未发布实现候选
77

8-
(空 — v1.15.3 发布候选已归档)
8+
- Codex 宿主 MCP:`init/update --codex` 部署 `.claude/mcp/*` 并向 owner `.codex/config.toml` 幂等 merge `BEGIN DEVCODEX-MCP-MANAGED` 块(devcodex-memory + profile,INPUT_ROOT=owner 根);保留用户其它 mcp_servers;CLI 行为测试与 README 矩阵同步。需求:`Codex宿主MCP配置与分发` CP1 已确认。
9+
- Codex MCP 复审跟进:`doctor` 纠正「不自动写 MCP」反事实文案;`codexConfigState.mcp` 探测 managed block + server 文件存在;website development 文档与 README 对齐;测试覆盖 config 备份与 doctor mcp.status=ok。
10+
- PF-175 / PI-155 **ArtifactPathColumnGate**:用户面交付项强制 `路径:` 列(默认 workspace-relative portable);Rich 语义链接可与路径列并存且无冗余 `绝对路径:` 行;同步 `user-visible-output-contract``02-output-paths`、delivery-checklist prompt、`classifyArtifactPathColumnSample` 与 visible-output 测试。
11+
- 消费者同步:`01-common`、always-on `instructions.md``website/docs/specs/report-output-flow.md` 补路径列口径;ECR 后 `devcodex update` 同步部署副本。
912

1013
## 历史锚点索引(已随 v1.15.0 / v1.15.1 / v1.15.2 / v1.15.3 发布或归档)
1114

hooks/_runtime/visible-output-contract.cjs

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -606,23 +606,70 @@ function portableTarget(filePath, workspaceRoot) {
606606
return String(filePath).replace(/\\/g, '/')
607607
}
608608

609-
function renderArtifactItem(item, capability, tier) {
609+
/**
610+
* PF-175 ArtifactPathColumnGate: always expose a path cell.
611+
* Default = workspace-relative portable; absolute only when fallback/failed/outside.
612+
* Rich clickable may still use absolute href for openability; path cell stays portable unless absolutePathFallback.
613+
*/
614+
function resolveArtifactPathCell(item, capability) {
610615
const portable = portableTarget(item.canonicalPath, capability?.workspaceRoot)
611-
const target = tier === 'rich-markdown' && capability?.mode === 'clickable'
612-
? String(item.canonicalPath).replace(/\\/g, '/')
616+
const absolute = String(item.canonicalPath || '').replace(/\\/g, '/')
617+
const forceAbsolute = capability?.absolutePathFallback === true ||
618+
capability?.mode === 'failed' ||
619+
capability?.targetRelation === 'outside-workspace'
620+
return {
621+
portable,
622+
absolute,
623+
pathCell: forceAbsolute ? absolute : portable,
624+
forceAbsolute
625+
}
626+
}
627+
628+
function renderArtifactItem(item, capability, tier) {
629+
const { portable, absolute, pathCell, forceAbsolute } = resolveArtifactPathCell(item, capability)
630+
const linkTarget = tier === 'rich-markdown' && capability?.mode === 'clickable'
631+
? absolute
613632
: portable
614-
const suffix = ` — ${item.purposeText};操作:${item.userAction}`
633+
const purpose = text(item.purposeText) || '用途未标注'
634+
const action = text(item.userAction) || '查看'
635+
// Path column always present (PF-175); not the same as legacy bare absolute-only lists.
636+
const pathSuffix = `;路径:\`${pathCell}\`;操作:${action}`
615637
if (tier === 'plain-text' || capability?.mode === 'plain' || capability?.mode === 'failed') {
616-
const locator = capability?.absolutePathFallback ? String(item.canonicalPath).replace(/\\/g, '/') : portable
617-
const fallback = capability?.absolutePathFallback ? `;fallback:${capability.fallbackReason}` : ''
618-
return `- ${item.displayName}${suffix} [${locator}]${fallback}`
638+
const fallback = capability?.absolutePathFallback && capability?.fallbackReason
639+
? `;fallback:${capability.fallbackReason}`
640+
: ''
641+
return `- ${item.displayName}${purpose}${pathSuffix}${fallback}`
642+
}
643+
const escapedTarget = /\s/.test(linkTarget) ? `<${linkTarget}>` : linkTarget
644+
let line = `- [${item.displayName}](${escapedTarget}) — ${purpose}${pathSuffix}`
645+
// Absolute line only when path cell is already absolute and reason must stay explicit for failed surfaces.
646+
if (forceAbsolute && capability?.absolutePathFallback && capability?.fallbackReason) {
647+
line += `\n 绝对路径:${absolute}${capability.fallbackReason})`
619648
}
620-
const escapedTarget = /\s/.test(target) ? `<${target}>` : target
621-
let line = `- [${item.displayName}](${escapedTarget})${suffix}`
622-
if (capability?.absolutePathFallback) line += `\n 绝对路径:${item.canonicalPath}`
623649
return line
624650
}
625651

652+
/**
653+
* Free-text delivery/CP table classifier (PF-175).
654+
* @returns {'not-claimed'|'present'|'missing-path-column'|'legacy-bare-path'}
655+
*/
656+
function classifyArtifactPathColumnSample(sample) {
657+
const textSample = String(sample || '')
658+
if (!textSample.trim()) return 'not-claimed'
659+
// Legacy bare-path lists are a claim form even without allowed action headings.
660+
if (/(?:||)\s*[:]?\s*\n(?:[-*]\s*)?(?:[A-Za-z]:[\\/]|\/)/.test(textSample) &&
661+
!/||\|\s*\s*\|/.test(textSample)) {
662+
return 'legacy-bare-path'
663+
}
664+
const claimsDelivery = /|||||/.test(textSample)
665+
if (!claimsDelivery) return 'not-claimed'
666+
const hasPathColumn =
667+
/\s*[:]/.test(textSample) ||
668+
/\|\s*\s*\|/.test(textSample) ||
669+
/\s*\|\s*/.test(textSample)
670+
return hasPathColumn ? 'present' : 'missing-path-column'
671+
}
672+
626673
function renderVisibleEnvelope(envelope, { tier = null, compact = false } = {}) {
627674
const validContract = envelope?.validation?.valid === true
628675
if (!validContract) envelope = invalidEnvelope(envelope?.validation?.errors || ['envelope-invalid'])
@@ -685,9 +732,13 @@ module.exports = {
685732
createLinkCapabilityDecision,
686733
createVisibleEnvelope,
687734
buildSimpleGovernanceFastPathDecision,
735+
classifyArtifactPathColumnSample,
688736
digest,
689737
isSemanticDisplayName,
738+
portableTarget,
690739
projectUserFacingArtifactSet,
740+
renderArtifactItem,
741+
resolveArtifactPathCell,
691742
renderVisibleEnvelope,
692743
shouldUseCompact
693744
}

instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ DevCodexVisibleEnvelopeV1 · entry-check · [状态] · [semanticDigest]
713713
- **用户面禁止输出**:内部工作流 ID(`dev.docs`/`fix.default`)、原始工具参数 XML、内部路由标签、调试 JSON
714714
- 仅在用户明确追问内部分类/机制时才展开内部术语,且最小化展开
715715
- 涉及文件产物时,先形成内部 `ArtifactDeliveryManifestV1`,再由 `UserFacingArtifactSetV1` 确定性投影;默认不展示 session、daily、SUMMARY、task state、checkpoint、raw receipt/manifest/ledger
716-
- 可见项必须使用语义名称、用途、用户动作和稳定阅读顺序Rich 点击能力已验证时只显示一个语义链接,不重复绝对路径。绝对路径仅在用户要求、链接失败、工作区外、歧义或无法定位时 fallback
716+
- 可见项必须使用语义名称、用途、路径列、用户动作和稳定阅读顺序(ArtifactPathColumnGate:路径默认 workspace-relative portable)。Rich 点击能力已验证时名称用一个语义链接,可与 portable 路径列并存,不在路径列外重复 `绝对路径:`。绝对路径仅在用户要求、链接失败、工作区外、歧义或无法定位时进入路径列/fallback
717717
- Copilot / Codex 等非 Claude Code 宿主调用 DevCodex MCP 出现 `invoke` undefined 或工具桥接失败时,按宿主 MCP bridge 失败处理:停止重试同一 MCP,只执行一次 path-observable / instruction-fallback 的同计划有界读取,记录 `mcpFallback=used`;无法取得 Post 成功证据时保持 `unverified`,不得退化为整目录或整文件默认读取
718718
- Commit subject 只描述主变更,不堆叠背景/验证步骤
719719

instructions/01-common.instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ version: 1.15.3
2929
>
3030
> 上述"最小化展开"主要约束**面向用户的默认输出场景**;项目内 `dev` 模式下的规范优化、规则提升与实现讨论不受此条新增限制。
3131
>
32-
> ⚠️ **用户可见交付契约**:涉及入口/完成检查、确认、进度、结果、阻断或文件交付时,触发 `user-visible-output-contract`。所有持久化产物先进入 `ArtifactDeliveryManifestV1`,用户面只由 `UserFacingArtifactSetV1` 确定性投影;session、daily、SUMMARY、task/checkpoint 与 raw receipt/manifest/ledger 默认 internal-only。链接按已验证能力选择 clickable/portable/plain/failedRich clickable 不重复绝对路径,只有用户要求、链接失败、工作区外、歧义或无法定位时追加绝对路径 fallback。
32+
> ⚠️ **用户可见交付契约**:涉及入口/完成检查、确认、进度、结果、阻断或文件交付时,触发 `user-visible-output-contract`。所有持久化产物先进入 `ArtifactDeliveryManifestV1`,用户面只由 `UserFacingArtifactSetV1` 确定性投影;session、daily、SUMMARY、task/checkpoint 与 raw receipt/manifest/ledger 默认 internal-only。链接按已验证能力选择 clickable/portable/plain/failed。每项交付必须满足 **ArtifactPathColumnGate(PF-175)**:语义名称 + 用途 + **路径列**(默认 workspace-relative portable)+ 操作;自由文本表默认列=`语义名称|用途|路径|操作`Rich clickable 允许语义链接与 portable 路径列并存,且不得在路径列外再重复 `绝对路径:` 行;只有用户要求、链接失败、工作区外、歧义或无法定位时路径列/fallback 才使用绝对路径
3333
>
3434
> ⚠️ **MCP fallback**:Copilot / Codex 等非 Claude Code 宿主调用 DevCodex MCP 出现 `invoke` undefined、工具桥接不可用或 server 未连接时,视为宿主 MCP bridge 失败;停止重试同一 MCP,降级读取 Profile / SUMMARY / tasks 文件,并在报告或记忆中记录 `mcpFallback=used`
3535

instructions/02-output-paths.instructions.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -146,38 +146,52 @@ reports/<子目录>/<agent>/YYYYMMDD/NN--<简述>.md
146146
- `完成交付文件`
147147
- `阻断证据`
148148

149-
每项必须输出 `displayName + purposeText + userAction`。路径、文件名、CP 编号、版本或状态不能单独充当名称。当前消费者禁止使用含义不稳定的“主要产物”和“本次会话全部产物”;历史版本文档不回填。
149+
每项必须输出 `displayName + purposeText + userAction`,并满足 **ArtifactPathColumnGate(PF-175)**:每项必须有独立 **路径** 字段/列,默认 workspace-relative portable。路径、文件名、CP 编号、版本或状态不能单独充当名称。当前消费者禁止使用含义不稳定的“主要产物”和“本次会话全部产物”;历史版本文档不回填。
150+
151+
自由文本交付表默认列:
152+
153+
| 语义名称 | 用途 | 路径 | 操作 |
154+
|----------|------|------|------|
150155

151156
### LinkCapabilityDecision 客户端兼容矩阵
152157

153158
能力必须按当前 surface 的可验证证据选择,禁止只按宿主名称硬编码:
154159

155-
| capability mode | 主表示 | 绝对路径 fallback | 证据边界 |
156-
|---|---|---|---|
157-
| `clickable` | 单个语义 Markdown 链接 | 默认不显示 | 当前 surface 点击能力已验证;Rich 不得重复路径 |
158-
| `portable` | 工作区相对 Markdown 链接 | 默认不显示 | Markdown 可用但点击能力未知;这是未知宿主默认档 |
159-
| `plain` | 语义名称 + 可复制相对/短路径 | 默认不显示 | 终端或日志仅保证纯文本 |
160-
| `failed` | 语义名称 + 可复制定位 | 显示并记录原因 | 链接已失败或宿主无法定位 |
160+
| capability mode | 主表示 | 路径列(强制) | 绝对路径 fallback | 证据边界 |
161+
|---|---|---|---|---|
162+
| `clickable` | 单个语义 Markdown 链接(href 可为绝对以便打开) | portable 相对路径 | 默认不进路径列 | Rich 不得在路径列外再写 `绝对路径:` |
163+
| `portable` | 工作区相对 Markdown 链接 | portable | 默认不进路径列 | Markdown 可用但点击能力未知 |
164+
| `plain` | 语义名称 + 路径列 | portable 或短路径 | 默认不 | 终端/日志纯文本 |
165+
| `failed` | 语义名称 + 绝对定位 | 绝对路径 + reason | 显示 | 链接失败或无法定位 |
161166

162-
只有以下情况允许绝对路径 fallback:用户明确要求、链接实际失败、目标位于工作区外、路径歧义、宿主无法定位。用户未要求且 Rich 点击已验证时,同一文件只能出现一个可点击语义链接,不得再重复 `绝对路径:...`
167+
只有以下情况允许路径列使用绝对路径(或额外 `绝对路径:...`):用户明确要求、链接实际失败、目标位于工作区外、路径歧义、宿主无法定位。
163168

164169
`ArtifactLinkSet` 保留为可见集合的兼容投影名,不再是真相源;`ArtifactLinkSetDedupeGate` 执行规范化绝对路径去重,按 canonical path 合并同一物理文件。禁止 `file://`,禁止只输出裸文件名,禁止询问“是否需要打开”。
165170

166-
推荐 Rich 示例:
171+
推荐 Rich 示例(语义链接 + portable 路径列)
167172

168173
```markdown
169174
#### 完成交付文件
170-
- [最终执行与验证报告](E:/Worker/.devcodex/.../12--最终执行报告.md) — 汇总完成范围、验证结果和残余风险;操作:查看结论
175+
- [最终执行与验证报告](E:/Worker/.devcodex/devcodex-v1/reports/.../12--最终执行报告.md) — 汇总完成范围、验证结果和残余风险;路径:`.devcodex/devcodex-v1/reports/.../12--最终执行报告.md`;操作:查看结论
171176
```
172177

173-
Portable 示例保持同一语义项,只将 target 改为工作区相对路径
178+
自由文本表示例
174179

175180
```markdown
176-
- [最终执行与验证报告](.devcodex/devcodex-v1/.../12--最终执行报告.md) — 汇总完成范围、验证结果和残余风险;操作:查看结论
181+
#### 完成交付文件
182+
183+
| 语义名称 | 用途 | 路径 | 操作 |
184+
|----------|------|------|------|
185+
| 最终执行与验证报告 | 汇总完成范围与残余风险 | `.devcodex/devcodex-v1/reports/.../12--最终执行报告.md` | 查看结论 |
177186
```
178187

179-
Portable/Plain 在同一 semanticDigest 下只改变链接形式,不改变文件集合、顺序、状态或动作。legacy “主要产物 + 绝对路径”文本最多识别为 `unverified-legacy`,不能作为 verified delivery receipt。
188+
Portable 示例保持同一语义项,链接 target 与路径列均为工作区相对路径:
189+
190+
```markdown
191+
- [最终执行与验证报告](.devcodex/devcodex-v1/reports/requirements/codex/20260719/12--最终执行报告.md) — 汇总完成范围、验证结果和残余风险;路径:`.devcodex/devcodex-v1/reports/requirements/codex/20260719/12--最终执行报告.md`;操作:查看结论
192+
```
180193

194+
Portable/Plain 在同一 semanticDigest 下只改变链接形式,不改变文件集合、顺序、状态、动作或路径列语义。legacy “主要产物 + 绝对路径”文本最多识别为 `unverified-legacy`,不能作为 verified delivery receipt。当且仅当 fallback 激活时,可追加 `绝对路径:E:/...` 并记录 reason。
181195
### MCP profile fallback
182196

183197
若 Copilot / Codex 等非 Claude Code 宿主调用 `profile_load``profile_get_mode` 或其他 DevCodex MCP 工具时出现 `TypeError: Cannot read properties of undefined (reading 'invoke')`、工具桥接不可用、MCP server 未连接等错误,视为**宿主 MCP bridge 失败**,不得反复重试同一 MCP 调用。AI 必须立即降级:

prompts/delivery-checklist.prompt.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ applyTo: .devcodex/**/requirements/**
5757
4. 把所有 planned/observed/internal-delivered 文件登记到 `ArtifactDeliveryManifestV1`,rename/move/delete 保留 previousPath/tombstone;
5858
5.`UserFacingArtifactSetV1` 投影默认交付,检查 required hidden=0、`listed+remaining=total`;session/daily/SUMMARY/task/checkpoint/raw receipt/manifest/ledger 默认 internal-only;
5959
6. 可见项必须使用 displayName/purposeText/userAction 与稳定 readingOrder,禁止“主要产物”;
60-
7. 按当前 surface 的 `LinkCapabilityDecisionV1` 选择 rich/portable/plain/failed;Rich clickable 不重复绝对路径;
61-
8. 完成后在报告中记录 `delivery-checklist: PASS`、manifestId、setId、semanticDigest 与 reconciliation。
60+
7. 按当前 surface 的 `LinkCapabilityDecisionV1` 选择 rich/portable/plain/failed;Rich clickable 不在路径列外重复 `绝对路径:` 行;
61+
8. **ArtifactPathColumnGate(PF-175)**:每项必须有路径列/字段,默认 workspace-relative portable;自由文本表默认列=语义名称\|用途\|路径\|操作;
62+
9. 完成后在报告中记录 `delivery-checklist: PASS`、manifestId、setId、semanticDigest 与 reconciliation。

scripts/test-visible-output-contract.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ const {
99
INTERNAL_ARTIFACT_CLASSES,
1010
createArtifactDeliveryManifest,
1111
buildSimpleGovernanceFastPathDecision,
12+
classifyArtifactPathColumnSample,
1213
createLinkCapabilityDecision,
1314
createVisibleEnvelope,
1415
projectUserFacingArtifactSet,
1516
renderVisibleEnvelope,
1617
shouldUseCompact
1718
} = require('../hooks/_runtime/visible-output-contract.cjs')
18-
1919
const ROOT = path.resolve(__dirname, '..')
2020
const WORKSPACE = path.dirname(ROOT)
2121

@@ -211,13 +211,36 @@ for (const output of [richText, portableText, plainText]) {
211211
for (const item of entrySet.items) assert.match(output, new RegExp(item.displayName))
212212
assert.doesNotMatch(output, /|/)
213213
assert.doesNotMatch(output, /|/)
214+
// PF-175: path column required on all presentation tiers
215+
assert.match(output, /[:]/)
216+
assert.match(output, /\.devcodex[\\/]devcodex-v1/)
214217
}
215218
assert.doesNotMatch(richText, /[:]/)
216219
assert.doesNotMatch(portableText, /[:]/)
220+
// Rich may use absolute href for clickable open; path cell stays portable
217221
assert.match(richText, new RegExp(WORKSPACE.replace(/[\\/]/g, '[\\\\/]')))
218222
assert.doesNotMatch(portableText, new RegExp(WORKSPACE.replace(/[\\/]/g, '[\\\\/]')))
219-
assert.match(plainText, /\.devcodex[\\/]devcodex-v1/)
220223
assert.doesNotMatch(plainText, /####|\[[^\]]+\]\([^\)]+\)/)
224+
225+
// PF-175 free-text path column classifier
226+
assert.strictEqual(
227+
classifyArtifactPathColumnSample('#### 完成交付文件\n| 语义名称 | 用途 | 路径 | 操作 |\n| a | b | `.devcodex/x.md` | 查看 |'),
228+
'present'
229+
)
230+
assert.strictEqual(
231+
classifyArtifactPathColumnSample('#### 完成交付文件\n- [报告](x.md) — 用途说明;路径:`.devcodex/x.md`;操作:查看'),
232+
'present'
233+
)
234+
assert.strictEqual(
235+
classifyArtifactPathColumnSample('#### 完成交付文件\n- [报告](x.md) — 用途说明;操作:查看'),
236+
'missing-path-column'
237+
)
238+
assert.strictEqual(
239+
classifyArtifactPathColumnSample('主要产物:\n- E:/Worker/foo.md'),
240+
'legacy-bare-path'
241+
)
242+
assert.strictEqual(classifyArtifactPathColumnSample('随便聊聊'), 'not-claimed')
243+
221244
const failedEnvelope = createVisibleEnvelope({ ...baseInput, linkCapability: failed })
222245
assert.strictEqual(failedEnvelope.status, 'BLOCK')
223246
const failedForSurface = createLinkCapabilityDecision({

skills/report/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ reports/<子目录>/<agent>/YYYYMMDD/NN--<简述>.md
116116
- 报告涉及规范源、Skill、Hook、CLI、MCP、模板、部署副本、路径规则或 validate 语义变更时,仍需列出 SCV-0~SCV-7 证据;外部 finding intake 不得把报告结论当作已验证事实
117117
- 控制面报告若出现新增探针、黄色偏离或部署同步,必须单独写出部署同步证据与其他证据来源,不能只在摘要里带过
118118
- 报告末尾引用本次会话记忆路径
119-
- 回复末尾由 `user-visible-output-contract` 输出“完成交付文件”:每项使用语义 displayName、purposeText、userAction,并按 decision→result→evidence→optional 顺序。`ArtifactLinkSet` 只作兼容投影;Rich clickable 不重复绝对路径,只有用户要求、链接失败、工作区外、歧义或无法定位时追加 fallback(详见 [`02-output-paths.instructions.md`](../../instructions/02-output-paths.instructions.md)
119+
- 回复末尾由 `user-visible-output-contract` 输出“完成交付文件”:每项使用语义 displayName、purposeText、userAction,并强制 **路径列**(默认 workspace-relative portable,见 ArtifactPathColumnGate / PF-175),按 decision→result→evidence→optional 顺序。`ArtifactLinkSet` 只作兼容投影;Rich clickable 不在路径列外重复 `绝对路径:`,只有用户要求、链接失败、工作区外、歧义或无法定位时路径列/fallback 用绝对路径(详见 [`02-output-paths.instructions.md`](../../instructions/02-output-paths.instructions.md)
120120

121121
## 行数与拆分
122122

0 commit comments

Comments
 (0)