@geoly-ai/social-hub-cli 0.3.18 → 0.3.19

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cmd-manifest.json +2 -2
  3. package/dist/index.d.ts +24 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +69 -7
  6. package/dist/index.js.map +1 -1
  7. package/dist/scrape-output.test.d.ts +2 -0
  8. package/dist/scrape-output.test.d.ts.map +1 -0
  9. package/dist/scrape-output.test.js +58 -0
  10. package/dist/scrape-output.test.js.map +1 -0
  11. package/package.json +2 -2
  12. package/skills/manifest.json +1 -1
  13. package/skills/reddit-content-writing/SKILL.md +1 -1
  14. package/skills/reddit-content-writing/references/phase4-execution-table.md +24 -3
  15. package/skills/reddit-content-writing/references/phase4-writing.md +123 -3
  16. package/skills/reddit-content-writing/references/post-writing-templates.md +14 -6
  17. package/skills/reddit-delivery-check/SKILL.md +1 -0
  18. package/skills/reddit-delivery-check/references/phase5-delivery.md +7 -1
  19. package/skills/reddit-matrix.lock.json +8 -8
  20. package/skills/reddit-phase-brief/SKILL.md +25 -9
  21. package/skills/reddit-phase-brief/references/brand-SKU-PGC-rules.md +223 -36
  22. package/skills/reddit-phase-brief/references/phase4-brief-workflow.md +364 -18
  23. package/skills/reddit-phase-planning/SKILL.md +13 -0
  24. package/skills/reddit-phase-planning/references/phase4-execution-table.md +16 -2
  25. package/skills/reddit-phase-planning/references/phase4-planning.md +74 -15
  26. package/skills/reddit-phase-planning/references/scheduling-decision-procedure.md +386 -0
  27. package/skills/reddit-phase-research/SKILL.md +7 -1
  28. package/skills/reddit-phase-research/references/phase01-research.md +93 -29
  29. package/skills/reddit-strategy-orchestrator/SKILL.md +120 -18
  30. package/skills/reddit-strategy-shared/SKILL.md +4 -2
  31. package/skills/reddit-strategy-shared/references/content-diversity-contract.md +19 -3
  32. package/skills/reddit-strategy-shared/references/context-contract.md +45 -11
  33. package/skills/reddit-strategy-shared/references/evidence-policy.md +139 -0
  34. package/skills/reddit-strategy-shared/references/handoff-schemas.md +16 -1
  35. package/skills/reddit-strategy-shared/references/path-conventions.md +26 -4
  36. package/skills/reddit-strategy-shared/references/prerequisites/README.md +57 -0
  37. package/skills/reddit-strategy-shared/references/prerequisites/{reddit-master-canvas-agent-prompt-template.md → archive/reddit-master-canvas-agent-prompt-template.md} +7 -0
  38. package/skills/reddit-strategy-shared/references/prerequisites/{reddit-sop-v2-universal.md → archive/reddit-sop-v2-universal.md} +7 -0
  39. package/skills/reddit-strategy-shared/references/prerequisites/{reddit-strategy-prerequisites-read.md → archive/reddit-strategy-prerequisites-read.md} +7 -0
  40. package/skills/reddit-strategy-shared/references/prerequisites/{reddit-strategy-reuse-playbook.md → archive/reddit-strategy-reuse-playbook.md} +7 -0
  41. package/skills/reddit-strategy-shared/references/subreddit-research-profile.md +514 -0
  42. package/skills/reddit-strategy-shared/references/version-manifest.md +29 -2
  43. package/skills/reddit-voc-volume/matrix-contract.md +16 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scrape-output.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrape-output.test.d.ts","sourceRoot":"","sources":["../src/scrape-output.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ vi.mock("@geoly-ai/social-hub-sdk", () => ({
3
+ SocialHubClient: vi.fn().mockImplementation(() => ({})),
4
+ getSocialHubHealth: vi.fn(),
5
+ }));
6
+ const { formatScrapeFailure, formatScrapeError } = await import("./index.js");
7
+ describe("scrape CLI 输出", () => {
8
+ it("业务失败摘要带 outcome / 阶段 / request id / 证据", () => {
9
+ const line = formatScrapeFailure({
10
+ outcome: "login_wall",
11
+ finalUrl: "https://x.com/i/flow/login",
12
+ targetStatusCode: 200,
13
+ failureStage: null,
14
+ providerRequestId: "fc-req-9",
15
+ requestId: "req-1",
16
+ attempts: 1,
17
+ retryAfterMs: null,
18
+ classifierVersion: "scrape-classify-v1",
19
+ classificationEvidence: [
20
+ { ruleId: "login-final-url-path", signal: "login-path" },
21
+ ],
22
+ });
23
+ expect(line).toContain("outcome=login_wall");
24
+ expect(line).toContain("targetStatus=200");
25
+ expect(line).toContain("providerRequestId=fc-req-9");
26
+ expect(line).toContain("classifier=scrape-classify-v1");
27
+ expect(line).toContain("login-final-url-path(login-path)");
28
+ });
29
+ it("技术失败摘要把 provider typed error 挖出来,不再是通用错误", () => {
30
+ const line = formatScrapeError({
31
+ message: "HTTP 502: scrape service HTTP 500 [XHS_RENDER_FAILED]",
32
+ status: 502,
33
+ code: "SCRAPE_UPSTREAM_ERROR",
34
+ details: {
35
+ outcome: "upstream_error",
36
+ diagnostics: {
37
+ provider: {
38
+ providerStatusCode: 500,
39
+ providerErrorCode: "XHS_RENDER_FAILED",
40
+ providerRequestId: "fc-req-1",
41
+ providerScrapeId: "fc-scrape-2",
42
+ },
43
+ execution: { failureStage: "render", attemptCount: 2 },
44
+ },
45
+ },
46
+ });
47
+ expect(line).toContain("code=SCRAPE_UPSTREAM_ERROR");
48
+ expect(line).toContain("outcome=upstream_error");
49
+ expect(line).toContain("stage=render");
50
+ expect(line).toContain("providerErrorCode=XHS_RENDER_FAILED");
51
+ expect(line).toContain("providerScrapeId=fc-scrape-2");
52
+ expect(line).toContain("attempts=2");
53
+ });
54
+ it("没有结构化 details 时退回原始 message(不编造字段)", () => {
55
+ expect(formatScrapeError(new Error("boom"))).toBe("boom");
56
+ });
57
+ });
58
+ //# sourceMappingURL=scrape-output.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrape-output.test.js","sourceRoot":"","sources":["../src/scrape-output.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAElD,EAAE,CAAC,IAAI,CAAC,0BAA0B,EAAE,GAAG,EAAE,CAAC,CAAC;IACzC,eAAe,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;IACvD,kBAAkB,EAAE,EAAE,CAAC,EAAE,EAAE;CAC5B,CAAC,CAAC,CAAC;AAEJ,MAAM,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;AAE9E,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,IAAI,GAAG,mBAAmB,CAAC;YAC/B,OAAO,EAAE,YAAY;YACrB,QAAQ,EAAE,4BAA4B;YACtC,gBAAgB,EAAE,GAAG;YACrB,YAAY,EAAE,IAAI;YAClB,iBAAiB,EAAE,UAAU;YAC7B,SAAS,EAAE,OAAO;YAClB,QAAQ,EAAE,CAAC;YACX,YAAY,EAAE,IAAI;YAClB,iBAAiB,EAAE,oBAAoB;YACvC,sBAAsB,EAAE;gBACtB,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,EAAE,YAAY,EAAE;aACzD;SACF,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;QAC3C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,4BAA4B,CAAC,CAAC;QACrD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC,CAAC;QACxD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,kCAAkC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,IAAI,GAAG,iBAAiB,CAAC;YAC7B,OAAO,EAAE,uDAAuD;YAChE,MAAM,EAAE,GAAG;YACX,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE;gBACP,OAAO,EAAE,gBAAgB;gBACzB,WAAW,EAAE;oBACX,QAAQ,EAAE;wBACR,kBAAkB,EAAE,GAAG;wBACvB,iBAAiB,EAAE,mBAAmB;wBACtC,iBAAiB,EAAE,UAAU;wBAC7B,gBAAgB,EAAE,aAAa;qBAChC;oBACD,SAAS,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,EAAE;iBACvD;aACF;SACF,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,4BAA4B,CAAC,CAAC;QACrD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC;QACjD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,qCAAqC,CAAC,CAAC;QAC9D,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC,CAAC;QACvD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geoly-ai/social-hub-cli",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "type": "module",
5
5
  "description": "social-hub CLI for Social Ops Hub",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  "dependencies": {
24
24
  "commander": "^12.1.0",
25
25
  "@geoly-ai/social-hub-authz": "0.0.17",
26
- "@geoly-ai/social-hub-sdk": "0.0.55"
26
+ "@geoly-ai/social-hub-sdk": "0.0.56"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.10.2",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
3
  "package": "social-ops-hub",
4
- "cliVersion": "0.3.18",
4
+ "cliVersion": "0.3.19",
5
5
  "repository": "social-ops-hub",
6
6
  "publish": {
7
7
  "registry": "https://skill.sh",
@@ -29,7 +29,7 @@ This skill writes planned Reddit posts and comments. It does not redo strategy,
29
29
 
30
30
  ## Source Reference
31
31
 
32
- - `references/phase4-writing.md` — W-1 baseline verification, W-1.5 fingerprint collision check, W-3 StructureFamily confirmation (the family is pre-assigned by the Brief; writing confirms, never picks), ABU / AnchorSlot rules, W-5 structure-preserving rewrite handoff
32
+ - `references/phase4-writing.md` — W-1 baseline verification (all **10** baseline dimensions; the 10th is the em-dash / parenthesis usage distribution), **W-1.b-brand `BrandPlacementGate`** (title generation and `BrandEntryMode` must cite the brand-first-mention histogram / SRP `brandFirstMentionHistogram`; never derive title-brand exposure from the P-B label; `PGCApproved` and `BrandReviewPost` title exposure stays legal), W-1.5 fingerprint collision check, W-3 StructureFamily confirmation (the family is pre-assigned by the Brief; writing confirms, never picks), **G1 internal-label outbound check** (reader-facing text carries only the Brief's `ReaderVisiblePremise`; fanout coverage as query-as-content stays legal, and the Feishu `[Fanout]` red line is an internal annotation), **G4 material-modality drift check** (writing/rewrite may never upgrade an `Optional` selling point into a required one), ABU / AnchorSlot rules, W-5 structure-preserving rewrite handoff
33
33
  - `references/phase4-execution-table.md` for the **core 16-field schema + 3 audit control columns** (`RewriteModule`, `AnchorSlot`, `SecondaryAnchorSlot`), the three independent routing axes (`客户内容原型` / `主帖型代码` / `StructureFamily`), unmapped-prototype blocking, complement rules, EPA control records, and batch-map gate
34
34
  - `references/post-writing-templates.md` — **the StructureFamily library (16 new families; 31 in total with the 15 in `writing-examples.md`) and its hard selection order**, ContentFunction minimum-information requirements, real-limitation (caveat) rules, and the 0–2 cap on surface colloquial signals
35
35
  - `references/writing-examples.md` — **15 StructureFamily reference posts with routing metadata**; this is a routing target, not optional reading. Every piece must inherit one family's information topology
@@ -1,6 +1,13 @@
1
- # Phase 4.0 — 执行表详细规则
1
+ # Phase 4.0 — 执行表详细规则(写作侧副本)
2
2
 
3
3
  > Phase 4.0 是内容写作的零号门禁,所有规则必须在第一个字正文写出之前满足。
4
+ >
5
+ > ⚠️ **主从关系(本文件是副本,不是权威):** 执行表 schema 与门禁的权威版本是
6
+ > `../../reddit-phase-planning/references/phase4-execution-table.md`(配 `scripts/phase406_verify.py`)。
7
+ > 本副本只为写作阶段就近查阅而存在;**两处冲突时一律以 planning 版为准**,
8
+ > 不要在本文件里就地改 schema。已知漂移(尚未同步、请按 planning 版执行):
9
+ > `RewriteModule` 的两层取值(`<RewriteRoute 标志集> · <SemanticModule>`,用功能语义名、
10
+ > **禁止 PROMPT 编号**)与相应的原型→帖型/`StructureFamily` 示例。
4
11
 
5
12
  ---
6
13
 
@@ -109,9 +116,23 @@ C024–C040:外部评论(W1→W2→W3→W4 顺序)
109
116
 
110
117
  执行表填写完毕后,动笔前必须运行以下聚合检查:
111
118
 
112
- | 检查项 | 阈值 | 不达标处理 |
119
+ > **口径以 D6 为准(`reddit-phase-planning/references/scheduling-decision-procedure.md`)。**
120
+ > 下面三条是**旧的硬编码启发式,已被 Phase 4.0.0.4 的 D6 锁定方案取代**,现在只按 `SRPMode` 分档生效:
121
+ >
122
+ > | `SRPMode` | 效力 |
123
+ > |---|---|
124
+ > | `Fallback`(该板块无 SRP) | **硬约束**,不达标按右列处理 |
125
+ > | `Full` / `Provisional`(该板块有 SRP) | **warning-level sanity check**:越界不否决方案,但必须在 D6 方案的约束快照里标注越界项与理由,交用户锁定;板块上限以 D5 的 `capacityMax` 为准 |
126
+ >
127
+ > **作用域:** 前两条按**板块**判(用该板块自己的 `SRPMode`);「子版覆盖数 ≥ 4 个」是**方案级**
128
+ > 约束,仅当方案内全部板块均为 `Fallback` 时才硬执行,混合模式下为 warning-level。
129
+ >
130
+ > 这组数字按「总帖 20 条」标定,对显著不同的总量不具尺度一致性。
131
+
132
+ | 检查项 | 阈值(`SRPMode: Fallback` 下为硬约束) | 不达标处理 |
113
133
  |--------|------|----------|
114
134
  | 单一子版帖子数占比 | ≤ 40% | 将超量帖子移到 Phase 2 缺口矩阵引用次数排名第二的子版 |
135
+ | 核心子版帖子数 | ≤ 8 条(总帖 20 条时) | 同上 |
115
136
  | 子版覆盖数 | ≥ 4 个不同子版 | 从 Phase 2 社区缺口矩阵补充 |
116
137
 
117
138
  ---
@@ -278,7 +299,7 @@ Geoly MCP → get_prompt_citations(
278
299
  [ ] 主帖含品牌词总数已统计并与 Master **Summary** 约定一致(差值=0)
279
300
  [ ] 外部评论 URL 均已在执行表中填写真实 URL,无「URL待确认」占位符
280
301
  [ ] 外部评论已建立「外部核验」控制列或独立 EPA 记录(不计入核心 16 字段主 schema),动笔后发布前须逐条填入核验结果
281
- [ ] 单一子版帖子占比 ≤ 40%,子版覆盖数 ≥ 4
302
+ [ ] 板块分布已按 D6 口径核对:单板块项(单一子版占比 ≤ 40% / 核心子版 ≤ 8 条)按该板块自己的 `SRPMode` 判——`Fallback` 为硬约束,`Full` / `Provisional` 以 D5 `capacityMax` 与 D6 锁定方案为准;「覆盖 ≥ 4 个」是方案级约束,仅全部板块为 `Fallback` 时硬执行。warning-level 越界项已在约束快照中标注理由(不否决方案)
282
303
  [ ] A/B 两组未在同一线程各发 C-B
283
304
  [ ] 【方案E】补偿规则评估已完成(全部 UGC P-NB 帖逐行检查 Rule-1;全部 C-NB 外部评论逐行检查 Rule-2)
284
305
  [ ] 【方案E】触发补偿规则的行已在执行表「写作说明」列注明处理方式
@@ -41,7 +41,7 @@
41
41
  | 段落数 | 中位数 | 决定分段密度 |
42
42
  | 标题长度与标题句式分布 | 各句式(陈述结论 / 问句 / 场景 / 数字背书 / 时长锚定)占比 | 决定标题形态,避免全批同一句式 |
43
43
  | 列表 / 小标题使用率 | 百分比(多少比例的帖使用) | 决定是否分节;**不再默认「分节更好」** |
44
- | 品牌首次出现位置分布 | 直方图(标题 / 首段 / 中段 / 结论 / 不出现) | 校准 `BrandEntryMode`,替代「不早于第二段」 |
44
+ | 品牌首次出现位置分布 | **四档**直方图(标题 / 首段 / 中段 / 结论;分母 = `BrandMentionSampleCount`)+ `ComparableSampleCount` + `NoBrandMentionRate`(「不出现」由它表示,**不是直方图的一档**) | **`BrandPlacementGate` 的唯一位置来源**(见下方 W-1.b-brand),校准 `BrandEntryMode`,替代「不早于第二段」 |
45
45
  | 提问式结尾比例 | 百分比 | 校准 `EndingMode`;比例低时不得默认加问句结尾 |
46
46
  | 是否常见 TL;DR | 百分比 | 决定是否使用顶部 TL;DR(同时是 AnchorSlot 依据) |
47
47
  | 第一人称比例 | 百分比 | 判断该板块的叙述人称惯例。**baseline 第一人称率高 ≠ 可以补造经历**——没有真实经历素材时,正确做法是换 `ContentFunction` / `EvidenceShape` 或退回补素材,不得虚构 |
@@ -50,6 +50,70 @@
50
50
 
51
51
  另附 **2–3 条 verbatim `selftext_excerpt`**(原文抄录,禁止「基于 VOC 风格自行创作」),用于校准腔调而非结构。
52
52
 
53
+ **W-1.b-brand — `BrandPlacementGate`(G2,硬门;上表第 5 行由「校准依据」升级为门)**
54
+
55
+ > **出处(真实返工):** 需求方质问「**为什么你把 P-B 的标题都统一加上了 aporro 这个
56
+ > 品牌了?**」——P-B 的含义是「帖子中包含品牌词」,**并不代表品牌名必须在标题里出现**,
57
+ > 那样显得很广告。修正后的口径:品牌词出现在 P-B 帖的**正文 body 中**,标题中通常不写
58
+ > 品牌名,**除非是 Brand Review / 专门讨论该品牌的帖子**。
59
+ >
60
+ > **机制早就有**(上表第 5 行的直方图),缺的是门与触达。本节补的是门。
61
+
62
+ **规则(writing 侧为消费与校验,权威定义在 `reddit-phase-brief` 的
63
+ `references/brand-SKU-PGC-rules.md` §2.0 / §2.5):**
64
+
65
+ 1. 任何**标题生成**与任何 `BrandEntryMode` 的确认,都必须引用 Brief 传下来的位置依据,
66
+ **按行类型分三支校验(不得一刀切要求自有 Basis):**
67
+ - **P-B / PGC 主帖** → 校验自有 `BrandPlacementBasis` 全字段;
68
+ - **P-NB 主帖** → 接受 `BrandPlacementBasis: N/A(P-NB · 主帖不露品牌)`,不再要求直方图;
69
+ - **C-B 评论** → 校验 `InheritedBrandPlacementBasis`(配对 P-NB 填 `N/A(PairedPostIsP-NB)`、
70
+ 外部评论填 `N/A(ExternalComment)`)+ `CommentPlacementDecision`,**评论不要求自有直方图**。
71
+
72
+ `BrandPlacementBasis`(其 `SourceRef` 为 baseline 卡的 `SampleSetID` 的品牌位置直方图,
73
+ `SourceKind: Exception`(持有有效 `BrandTitleExposureException` 时的合法来源),
74
+ 或板块级 SRP 的 `brandFirstMentionHistogram`,契约见
75
+ `../../reddit-strategy-shared/references/subreddit-research-profile.md`
76
+ —— **该板块暂无可用 SRP 实例时以 baseline 卡为主来源,不得因其缺失而阻断**)。
77
+ 2. **拦截条件(命中任一即 `BlockingGap: BrandPlacementUnsupported`,退回
78
+ `reddit-phase-brief`,writer 不得本地修):**
79
+ - 存在「**因为这行标记 P-B,所以标题写品牌**」的推导;
80
+ - **P-B / PGC 主帖**的 `BrandPlacementBasis` 缺失,或 `SelectedPlacement` 与 basis 不一致
81
+ (**P-NB 主帖的 `N/A(P-NB · 主帖不露品牌)` 与 C-B 评论的
82
+ `InheritedBrandPlacementBasis` + `CommentPlacementDecision` 均为合法填法,
83
+ 不得据本条拦截**;C-B 缺这两项才拦截);
84
+ - 直方图「标题」档 < 20% 却把品牌名写进标题,且无 `BrandTitleExposureException`;
85
+ - `DecisionStatus: NoComparableEvidence`(`BrandMentionSampleCount = 0`)却照常赋值 ——
86
+ **0 是「没有可比样本」,不是「不允许标题」,也不是一个可以照抄的保守位置**。
87
+ ⚠️ **持有有效例外时 Brief 会记 `DecisionStatus: Exception`(优先于
88
+ `NoComparableEvidence`)+ `SourceKind: Exception` + `SourceRef: <ExceptionRef>`
89
+ —— 这种行不得因「没有 SampleSetID / 没有直方图」被拦**;
90
+ - `DecisionStatus: Sparse`(1–4 条)而 Brief 未同时给出三个计数、更保守(更晚)的一档
91
+ 与 `SparseApprovedBy`。
92
+ > **直方图口径:** `BrandFirstMentionHistogram` 只有**标题/首段/中段/结论四档**,
93
+ > 分母是 `BrandMentionSampleCount`;「不出现」由 `NoBrandMentionRate` 单独表示,
94
+ > **不是直方图的一档**。
95
+ > **C-B 评论不自带直方图**:评论用配对 P Key 的 `InheritedBrandPlacementBasis` +
96
+ > `CommentPlacementDecision`(外部评论为 `N/A(ExternalComment)`)。
97
+ 3. ⚠️ **合法例外必须放行(不得一刀切):** `BrandTitleExposureException` 为
98
+ `PGCApproved`(官方/PGC 账号 + 已过 precedent check 且有先例 URL + 版规允许
99
+ vendor/official + flair/disclosure/frequency 满足)或 `BrandReviewPost`(确为围绕该
100
+ 品牌/产品本身的帖,由 `PrimaryPostType` / `WritingVariant` + 选题证据判定,
101
+ **不得自造 `ContentFunction: Review`**)时,**标题出现品牌名合法**,不得因直方图不支持
102
+ 而拦截。例外须带 `ExceptionBasis` / `ExceptionScope` / `ExceptionApprovedBy` /
103
+ `ExceptionApprovedAt` 四项,且不得越过禁止推广板块与披露要求。
104
+ 4. `BrandEntryMode` 本身仍是 `LockedFingerprintField`:位置变更一律退回 Brief 重分配。
105
+ 5. ⚠️ **既有 P-B / PGC 行的 `SelectedPlacement` 只能是 标题 / 首段 / 中段 / 后段**
106
+ (直方图档位按「结论 → 后段」换算)。D 类板块或 `NoBrandMentionRate ≥ 0.8` 导致主帖
107
+ 无法露出时,**既有 P-B / PGC 行走 `BlockingGap` 回 Brief / 规划**,不得静默落到
108
+ 「不出现」或挪到评论;「不出现」只对规划时已是 P-NB 的行合法。
109
+ 把品牌挪到评论或干脆不出现,等于让这行变成 P-NB + C-B(帖—评互补规则见下文),
110
+ **那是配额与行类型变更** —— writing 阶段一律 `BlockingGap` 退回 Brief / 规划,
111
+ 不得自行执行。
112
+
113
+ ⚠️ **本门只处理「位置由什么决定」,不处理「要不要有品牌」。** P-B/P-NB/C-B/C-NB 配额
114
+ 语义一律不变;位置不成立时退回 Brief,**不得**在 writing 阶段把 P-B 改成 P-NB、
115
+ 改配对或改配额。
116
+
53
117
  **W-1.c 若需要补采(仅在 Brief 授权重采时):**
54
118
 
55
119
  ```bash
@@ -218,6 +282,13 @@ SampleSetID:
218
282
  AnchorSlot:
219
283
  AllowedStructuralChanges:
220
284
  ForbiddenStructuralChanges:
285
+ BrandPlacementBasis: # G2:主帖用 SelectedPlacement + DecisionStatus + 例外(rewrite 不得改品牌位置)
286
+ InheritedBrandPlacementBasis: # G2:**评论交接时用这两项代替上一行**(配对为 P-NB 填 N/A(PairedPostIsP-NB),
287
+ CommentPlacementDecision: # 外部评论填 N/A(ExternalComment))
288
+ ReaderVisiblePremise: # G1:rewrite 只能表达这个,不得引入内部标签
289
+ MaterialModalitySnapshot: # G4:逐条 MaterialID: OriginalModality → EffectiveModality
290
+ # + ModalityStatus / ModalityUpgradeRef / ModalityDowngradeRef /
291
+ # DowngradeSource / ApprovedBy(或 LedgerRef);rewrite 不得升级/降级
221
292
  ```
222
293
 
223
294
  rewrite **默认只能**:修事实表达、修营销腔、修可读性、修明确触发的问题。
@@ -255,8 +326,10 @@ Hub pass 必须发生在最后一次正文修改之后。
255
326
  |--------|------|
256
327
  | 标题/任务方向 | 与 Brief 一致或更具体,不得偏离话题 |
257
328
  | 大纲与撰写逻辑 | 段落结构按 Brief 展开 |
258
- | 品牌词嵌入 | P-B/C-B Brief 位置与方式;符合帖—评互补 |
259
- | 方向选取理由 | 正文须落实 Brief 所指向的缺口/Prompt |
329
+ | 品牌词嵌入 | P-B / PGC 主帖按 Brief 的 `SelectedPlacement` + `BrandPlacementBasis`;C-B 评论按 `InheritedBrandPlacementBasis` + `CommentPlacementDecision`(W-1.b-brand);符合帖—评互补 |
330
+ | 方向选取理由 | 正文须落实 Brief 所指向的缺口/Prompt(**该理由属内部面,不得写进正文**,见下方 G1) |
331
+ | 叙事分层 | 正文只表达 Brief 的 `ReaderVisiblePremise`;`InternalCoverageIntent` 不得进入读者可见文本 |
332
+ | 素材 modality | 按 Brief 的 `EffectiveModality` 写;writing **不得升级**(见下方 G4) |
260
333
 
261
334
  Brief 与执行表冲突时:**暂停动笔**,先修订 Brief 或执行表并重新走 Brief 门禁。
262
335
 
@@ -265,6 +338,47 @@ Brief 与执行表冲突时:**暂停动笔**,先修订 Brief 或执行表并
265
338
  [ ] Brief 审核状态已满足(甲方:Brief 已通过 / 内部:确认 Brief · 可动笔)
266
339
  ```
267
340
 
341
+ ### G1 — 内部标签 outbound 检查(每条内容成稿后必做)
342
+
343
+ > Brief 侧的分层定义见 `reddit-phase-brief` 的
344
+ > `references/phase4-brief-workflow.md` 步骤 B-1.6。writing 侧只做 outbound 校验。
345
+
346
+ **扫描面(只扫这些,且只扫将要发到 Reddit 的文本):** 标题、正文 body、评论正文。
347
+
348
+ **拦截:** 出现 FAQ 编号 / 「响应·覆盖 FAQ」字样 / GEO·searchQuery·关键词标签原样 /
349
+ Key 编号 / `prompt_id` / 缺口象限码 / `SampleSetID` / 内部代号 → `NeedsFix`,
350
+ 改写成 `ReaderVisiblePremise` 的自然表达。
351
+
352
+ > **判据继承 B-1.6,不是无条件字符串黑名单:** 只有当这些字符串**作为内部标签、查询标签
353
+ > 或策略动作**出现时才拦截。**产品型号、真实用户原话引用、以及碰巧形似内部编号的自然
354
+ > 语句(型号 `A-07`、容量 `Q12`、「这几个问题我也被问过」)不得因形状相似而拦截。**
355
+ > 判定时以本项目登记在 `InternalCoverageIntent` 里的实际 token 为准。
356
+
357
+ ⚠️ **不误伤(三条,必须完整读):**
358
+
359
+ 1. **fanout 子查询「在正文中自然覆盖」仍然合法**(见下文「动笔顺序规则」B 组条款)。
360
+ 那是 **query-as-content**(把查询的语义用人话写出来),本门只禁
361
+ **query-as-label**(把查询字符串当角度或标题贴上去)。
362
+ 2. **飞书文档里的 `[Fanout] <查询文本>` 标红行是文档内部标注**(见下文红色标记规则),
363
+ 给内容同事看、**不发到 Reddit**,不属于读者可见叙事;但 outbound 检查必须确认它
364
+ **没有**混进 Reddit 的 title / body / comment。
365
+ 3. **只扫 outbound 文本,不扫 Brief、指纹索引、执行表、审计控制列、rewrite handoff**
366
+ —— 这些位置本来就该保留内部标签。
367
+
368
+ ### G4 — 素材 modality drift 检查(每条内容成稿后必做)
369
+
370
+ > 台账与授权规则见 `reddit-phase-brief` 的 `references/phase4-brief-workflow.md` 步骤 B-1.4。
371
+
372
+ - 逐条比对本篇用到的 `MaterialID` 的 `EffectiveModality` 与实际落笔:
373
+ **writing 与 rewrite 一律不得升级**(`Optional → Preferred/Required` 等)。
374
+ 出现「必须嵌入 / 强制带上这个卖点」的自发要求 → **拦截并回落**到 Brief 传下的值。
375
+ - `Optional` 的卖点**不满足条件就不写,不算缺陷**;不得为「凑齐卖点」硬塞。
376
+ - `Required` 项在成稿中缺失 → `BlockingGap`(不是「本篇没写而已」),退回 Brief。
377
+ - 需要变更 modality(升级或降级)→ 退回 `reddit-phase-brief` 走授权流程
378
+ (升级记 `ModalityUpgradeRef`,降级记 `ModalityDowngradeRef` + `DowngradeSource`,
379
+ 两者均须 `ApprovedBy` ≠ 生成 agent);
380
+ **用户明确标 `Optional` 的项目永不由本阶段升级**。
381
+
268
382
  ---
269
383
 
270
384
  ## Brand Risk 预防评估(每批终稿前必做)
@@ -394,6 +508,8 @@ FixOwnerSkill: reddit-phase-brief
394
508
  - **任何正文:** 须 Phase 4 Brief 门禁已通过、Phase 4.0.6 通过,且用户已回复「确认动笔 · Batch N」
395
509
  - **A 组(P-NB):** 可在 Phase 4.0.5 未完成时进入 4.0.6,但 **B 组 Keys 须等 fanout 补齐后再写**
396
510
  - **B 组(P-B):** fanout 列齐全 + 4.0.6 通过后,按批次 Map 撰写;正文须自然覆盖 fanout 子查询
511
+ —— **query-as-content(把查询语义用人话写出来)合法;query-as-label(把查询字符串原样
512
+ 当标题或角度贴上去)违反 G1**
397
513
 
398
514
  ---
399
515
 
@@ -598,6 +714,8 @@ else:
598
714
  [ ] W-3 本批每篇均已确认 Brief 预分配的 StructureFamily,并已读对应范文的元数据块
599
715
  [ ] 本批未出现伪造社区历史、刻意 typo、虚构身份信号、虚构缺点
600
716
  [ ] 本批开场句未直接复用范文或模板中的示例句(示例即种子)
717
+ [ ] G2:本批每条 P-B / PGC 主帖均带 Brief 的 BrandPlacementBasis(C-B 评论带 InheritedBrandPlacementBasis + CommentPlacementDecision),SelectedPlacement 与 basis 一致;无「因为标记 P-B 所以标题写品牌」的推导;标题露出品牌的行有直方图支持(标题档 ≥ 20%)或四项齐全的 BrandTitleExposureException;Sparse 行满足三条件;DecisionStatus: Exception 的行不因缺 SampleSetID/直方图被拦
718
+ [ ] G4:本批每条素材的 EffectiveModality 已从 Brief 取到,未在写作阶段升级;Optional 项未被当成必写
601
719
  ```
602
720
 
603
721
  ---
@@ -639,6 +757,8 @@ lark-cli docs 写入富文本时,对红色片段使用 inline 颜色属性(`
639
757
  [ ] B 组帖:ABUVisibilityGate 按长度档通过(中帖唯一 ABU 不得仅落在结论段且仅靠标题 cue;长 B 组帖只有末尾 ABU 须记录例外理由)
640
758
  [ ] B 组帖:飞书文档 Fanout Query 行已标红(或已用【红色标注:...】文字替代)
641
759
  [ ] B 组帖:飞书文档 ABU 已内联标红(或已用【红色标注:...】文字替代)
760
+ [ ] G1 outbound:标题 / 正文 / 评论中无**作为内部标签出现的** FAQ 编号、「响应·覆盖 FAQ」字样、GEO/searchQuery/关键词标签原样、Key/prompt_id/象限码(按 token registry + 标签语境判定;**真实产品型号、用户原话、形似编号的自然语句不算违规**);`[Fanout]` 行只存在于飞书文档,未混入 Reddit payload
761
+ [ ] G4:Required 素材均已落笔(缺失=BlockingGap);无未授权的 modality 升级或降级(升级须 ModalityUpgradeRef,降级须 ModalityDowngradeRef + DowngradeSource + ApprovedBy,或可追溯的 LedgerRef)
642
762
  ```
643
763
 
644
764
  ## 外部热帖评论前置核查(C-B / C-NB 外部评论每条必做,动笔前完成)
@@ -10,9 +10,9 @@
10
10
  > 4. **禁止为了不同而不同**:结构在 **Brief 阶段**按 `content-type-routing-contract.md` 的有序过滤收敛(见下文 §C),写作阶段只确认,不做随机轮换。
11
11
 
12
12
  **适用范围:** 任意品牌 Reddit GEO 内容;示例含 eSIM、耳机品类,换品类时替换关键词即可。
13
- **数据来源:** reddit-strategy-reuse-playbook.md(§5、§10、§13、§15);Reddit VOC 三轮采样(2026-06-10,使用 `reddit-voc-volume`,r/eSIMs / r/Earbuds / r/HeadphoneAdvice / r/JapanTravel / r/solotravel / r/digitalnomad / r/BudgetAudiophile;含评分、upvote_ratio 真实数据)。
13
+ **数据来源:** Reddit VOC 三轮采样(2026-06-10,使用 `reddit-voc-volume`,r/eSIMs / r/Earbuds / r/HeadphoneAdvice / r/JapanTravel / r/solotravel / r/digitalnomad / r/BudgetAudiophile;含评分、upvote_ratio 真实数据)。本文的模板与「引用倾向」是这批采样的归纳,**不是**规则手册的转述。
14
14
 
15
- **与主文档关系:** 执行顺序仍以 **01 执行入口**、**02 规则手册** 为准;本文是写帖侧的「格式 + 品牌嵌入」速查,不替代 Phase 0–5 全流程。
15
+ **与主文档关系:** Phase 顺序与门禁以 `../../reddit-strategy-orchestrator/SKILL.md`(Phase Orchestration / Phase Gates)为准;结构与帖型路由以 `../../reddit-strategy-shared/references/content-type-routing-contract.md`、差异化以 `../../reddit-strategy-shared/references/content-diversity-contract.md`、写作硬门禁以 `phase4-writing.md` 为准。本文是写帖侧的「格式 + 品牌嵌入」速查,不替代 Phase 0–5 全流程,也不是规则手册。
16
16
 
17
17
  ---
18
18
 
@@ -357,13 +357,21 @@ TL;DR 放最上;Background 段先写「near-launch 购买、替换 AirPod Pro
357
357
 
358
358
  ---
359
359
 
360
- ## 延伸阅读(本文件夹内)
360
+ ## 延伸阅读(active 权威文件)
361
+
362
+ > ⚠️ 旧版此处列的 `02-规则手册-reddit-strategy-reuse-playbook.md` 与
363
+ > `01-执行入口-reddit-sop-v2-universal.md` 已归档到
364
+ > `reddit-strategy-shared/references/prerequisites/archive/`(`AgentReadPolicy: DoNotRead`),
365
+ > **不得**再当作数据来源、规则手册或 Phase 顺序依据;引用即 gate failure。下表是它们的 active 接替者。
361
366
 
362
367
  | 文档 | 用途 |
363
368
  |------|------|
364
- | 02-规则手册-reddit-strategy-reuse-playbook.md | 完整 §5 模板学习、§10 帖型、§13 品牌规则、§14 评论 2:8 |
365
- | 01-执行入口-reddit-sop-v2-universal.md | Phase 顺序与门禁 |
366
- | 06-VOC采样技能-reddit-voc-volume-SKILL.md | 拉取 Top 引用帖 selftext、竞品 VOC |
369
+ | `../../reddit-strategy-orchestrator/SKILL.md` | Phase 顺序与门禁(Phase Orchestration / Phase Gates / Human Decision Points) |
370
+ | `phase4-post-rules.md` | 品牌词规则、P-NB→C-B 互补、评论 2:8 |
371
+ | `../../reddit-strategy-shared/references/content-type-routing-contract.md` | 客户内容原型 主帖型 StructureFamily 路由 |
372
+ | `../../reddit-strategy-shared/references/content-diversity-contract.md` | 指纹、滚动窗口、baseline manifest 与 `SampleSetID` |
373
+ | `phase4-writing.md` | 写作阶段硬门禁(W-1.a / W-1.b / W-1.b-brand 等) |
374
+ | `../../reddit-voc-volume/SKILL.md` | 拉取 Top 引用帖 selftext、竞品 VOC |
367
375
 
368
376
  ---
369
377
 
@@ -25,6 +25,7 @@ Mandatory before running the structure gates:
25
25
  - `memory/phase4-叙事指纹索引.md`
26
26
  - `memory/phase4-撰写brief.md` (for the `Planned` structure values and `SampleSetID`)
27
27
  - `memory/phase4-sku-基准表.md`
28
+ - `memory/phase4-board-quantity-plan.md` (per-board `SRPMode` + the locked `LockedPlanID`; needed to judge the board-distribution check, which is a hard constraint only under `SRPMode: Fallback`)
28
29
  - `memory/phase4-执行表.md`
29
30
  - `memory/phase4-撰写批次map.md`
30
31
  - all batch draft files
@@ -31,7 +31,13 @@
31
31
  [ ] Phase 4:正文按批次 Map 撰写,默认每批 8-10 条;帖评整组不拆时允许 8-12;末批可少于 8;无跳批丢包
32
32
  [ ] Phase 4.0:执行表编号无冲突(自评编号与外部评论编号无重叠)
33
33
  [ ] Phase 4.0:主帖含品牌词总数与 Master **Summary** 品牌词约定差值=0
34
- [ ] Phase 4.0:单一子版帖子占比 40%,子版覆盖数 ≥ 4
34
+ [ ] Phase 4.0:板块分布符合 D6 口径(**不是无条件硬门禁**;判据取自
35
+ `memory/phase4-board-quantity-plan.md` 的逐板块 `SRPMode` 与 `LockedPlanID`,
36
+ 该文件缺失才是阻断项)——
37
+ · 单一子版占比 ≤ 40% / 核心子版 ≤ 8 条(总帖 20 条时):**按该板块自己的 `SRPMode` 判**,
38
+ `Fallback` 为硬约束,`Full` / `Provisional` 降为 warning-level;
39
+ · 子版覆盖数 ≥ 4 个:**方案级**约束,仅当方案内全部板块均为 `Fallback` 时才硬执行,混合模式下为 warning-level;
40
+ · warning-level 项越界**不判不合格**,只需确认越界项已在 D6 方案约束快照里标注理由、方案已被用户锁定
35
41
  [ ] Brand Risk:`memory/phase4-brand-risk-review.md` 已存在或有明确 low-risk waiver;所有 HoldForCompliance / HoldForHumanApproval / DoNotPublish 行已处理,不在交付稿中
36
42
  [ ] Content Review:`memory/content-review-log.md` 已存在并覆盖每条交付帖子/评论;每条均有对应 Hub `executionStatus=completed` + `verdict=pass`,且最终交付审核为 `ReviewMode: AccountBound`、有真实 `accountId`、无未解决的 `brandFitReviewRequired=true` / 阻断型 `accountCandidateFeedback`;`ContentOnly` 只能作为内容交接结果,不能作为 Phase 5 发布通过;`degraded` / unavailable / 仅本地自检均不通过
37
43
  [ ] 草稿文档唯一性:不存在「【更新】版」「第二套」等平行节;所有 URL 已原地填入,无占位符
@@ -4,31 +4,31 @@
4
4
  "archiveSha256": "7af75a08d8779f0d4aa73e6fa0706e7fac82737da7eecc08adf8a02151619e0b",
5
5
  "skills": {
6
6
  "reddit-strategy-orchestrator": {
7
- "contentSha256": "501408ce9cd1be969ac91c672523cde18bdbb6118836e032539ef30af98f2711"
7
+ "contentSha256": "9a4fa3944ec3913a4e68cb1faf4d4a32ca04131fa4b0c37e054065f4e2c1a1ed"
8
8
  },
9
9
  "reddit-strategy-shared": {
10
- "contentSha256": "9ffdae74106e1b93b265bd89f6b0b1674f2b37a9dafd43c4a50129ac75f85862"
10
+ "contentSha256": "05433f0686955304b3ccaf728aacead4680a9b89a9adff4955111c837a441c4a"
11
11
  },
12
12
  "reddit-phase-research": {
13
- "contentSha256": "e1818757ae92019448a454b956cb38c9cabc7e3c4762da4a6e4a765d687b8dc6"
13
+ "contentSha256": "9f47b8eb0d1c1c194c685856242c9b22d190dfa8c6f572869e964239215795fe"
14
14
  },
15
15
  "reddit-phase-planning": {
16
- "contentSha256": "7568acf5b9e0ec9bd44deff09a49cadd80a7cf5acbe483c97f979f96ac9d962f"
16
+ "contentSha256": "8080adce62b6db65be3dbc0f37f162a81e382d6b64108f7352ec83c356664659"
17
17
  },
18
18
  "reddit-phase-brief": {
19
- "contentSha256": "841d9e9928126553af0211da7d74a69af92076fc00d800dff2243075359b2157"
19
+ "contentSha256": "6030d98d515355526e63d89246845dd18f0d0d2b3a2dbf71a2e9317eae31f54b"
20
20
  },
21
21
  "reddit-content-writing": {
22
- "contentSha256": "9b2f4cda552d5f76b3ab289504f84db599245e7245297a9d974cc55143872d8d"
22
+ "contentSha256": "b3a2219d2a328f76413cea34f0dfaa8588da635e3d644781d124e4d3b2a92e03"
23
23
  },
24
24
  "reddit-content-rewrite": {
25
25
  "contentSha256": "3e7df833877fc81a46d66ab7c827fac386a1413862724179e87570d773ab8935"
26
26
  },
27
27
  "reddit-delivery-check": {
28
- "contentSha256": "234d7fd1d8f2db68444674c20418c836c1b2488305b20399114dec89a0de32fc"
28
+ "contentSha256": "690609ca874e259800d02dbeebd707546d432bc24525a7c65ace0e0a7633ee78"
29
29
  },
30
30
  "reddit-voc-volume": {
31
- "contentSha256": "a5f21cc499688b245cfa3381ffdb88fa681cd5e93f3f1ef68d925b7ae220de2f"
31
+ "contentSha256": "496c0d57af7507d86a16bd4d06f3a5a017ca10b3dc4e3564d82b0c988e1c87ac"
32
32
  },
33
33
  "reddit-brand-risk-response": {
34
34
  "contentSha256": "dbba80345ebe68818a25e7604dfaa6951c6bf0e4d785d5013be7a06b22ec5b7c"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: reddit-phase-brief
3
3
  description: |
4
- Reddit Phase 4 Brief skill for the matrix SOP. Use after reddit-phase-planning completes Phase 4.0.6 and before reddit-content-writing starts. It assigns the target flair first, then establishes the flair-level subreddit baseline, then locks ContentFunction and StructureFamily, then the outline; it also creates board rationale, content schedule, per-Key title/direction/outlines, brand/SKU placement strategy, prewriting control fields (AssignedFlair / PostingTrigger), structure pre-assignment (OpeningMove / EvidenceShape / BrandEntryMode / EndingMode / LengthBand / SampleSetID / RecentCollisionCheck), and the approval gate. Trigger when the user says "确认进入 Phase 4 Brief", "开始 Brief", "产出 Brief 汇报稿", or when planning is complete but drafting has not started.
4
+ Reddit Phase 4 Brief skill for the matrix SOP. Use after reddit-phase-planning completes Phase 4.0.6 and before reddit-content-writing starts. It locks the target flair first by verifying planning's `ProposedFlair` from the scheduling ledger, then establishes the flair-level subreddit baseline, then locks ContentFunction and StructureFamily, then the outline; it also creates board rationale, content schedule, per-Key title/direction/outlines, brand/SKU placement strategy, prewriting control fields (AssignedFlair / PostingTrigger), structure pre-assignment (OpeningMove / EvidenceShape / BrandEntryMode / EndingMode / LengthBand / SampleSetID / RecentCollisionCheck), and the approval gate. Trigger when the user says "确认进入 Phase 4 Brief", "开始 Brief", "产出 Brief 汇报稿", or when planning is complete but drafting has not started.
5
5
  ---
6
6
 
7
7
  # Reddit Phase Brief
@@ -28,6 +28,7 @@ Then verify these project memory files exist:
28
28
  - `memory/phase4-执行表.md`
29
29
  - `memory/phase4-SKU品牌词映射.md`
30
30
  - `memory/phase4-撰写批次map.md`
31
+ - `memory/phase4-排期决策台账.md` with a `ProposedFlair` for every main-post Key in this batch (D2 output; source of `AssignedFlair`)
31
32
  - `memory/phase0.5-board-analysis.md`
32
33
  - `memory/phase1-VOC竞品缺口.md`
33
34
  - `memory/phase2-缺口分析.md`
@@ -59,7 +60,7 @@ guess the prototype from how the code reads.
59
60
  Read these local references before producing the brief:
60
61
 
61
62
  - `references/phase4-brief-workflow.md` for the full B-0 to B-3 workflow, templates, and gate checklist.
62
- - `references/brand-SKU-PGC-rules.md` before deciding brand/SKU placement for any P-B, C-B, or PGC row.
63
+ - `references/brand-SKU-PGC-rules.md` before deciding brand/SKU placement for any P-B, C-B, or PGC row. Read §2.0 (placement **source** = the brand-first-mention histogram; §2 A/B/C/D are risk **ceilings**) and §2.5 (comparable-sample semantics; `BrandMentionSampleCount = 0` means *no comparable evidence*, not *titles disallowed*) before A/B/C/D.
63
64
 
64
65
  ## Workflow
65
66
 
@@ -72,10 +73,18 @@ outline before the structure is decided from evidence.
72
73
  2. Confirm the number of Brief items in this batch.
73
74
  3. Load or initialize `memory/phase4-内容排期表.md`.
74
75
  4. Check historical `memory/phase4-撰写brief.md` for overlap before creating a new batch.
75
- 5. **Assign `AssignedFlair` (required, per main-post Key) before sampling.** flair 同时
76
- 决定合规资格、披露要求与触达,且决定 baseline 的采样口径。flair 必须取自该 subreddit
77
- 的现行 flair 列表(不得自造);板块无 flair 或允许不选时用哨兵值 `NoFlairAvailable` /
78
- `NoFlairRequired`(不得留空、不得用评论的 `N/A`);缺失 `BlockingGap`。评论 / 回复填 `N/A`。
76
+ 5. **Lock `AssignedFlair` (required, per main-post Key) before sampling by verifying the
77
+ planning-side `ProposedFlair`, not by choosing a new flair.** Read `ProposedFlair` for the
78
+ Key from `memory/phase4-排期决策台账.md` (produced by `reddit-phase-planning` D2, contract
79
+ `reddit-phase-planning/references/scheduling-decision-procedure.md`), then verify it against
80
+ the subreddit's **currently selectable** flair list and the account's eligibility. Verified →
81
+ `AssignedFlair = ProposedFlair`. Not verifiable → `FlairVerificationStatus: Rejected` +
82
+ `BlockingGap` back to planning for a new `ProposedFlair`; the Brief must **not** substitute a
83
+ flair of its own. The single exception is `ProposedFlair: Unknown(NoSRP)` (planning ran in
84
+ `SRPMode: Fallback`), where the Brief picks from the live flair list and writes the choice and
85
+ reason back to the ledger (`FlairVerificationStatus: FallbackAssigned`). Flairless boards use
86
+ the sentinels `NoFlairAvailable` / `NoFlairRequired` (never blank, never the comment `N/A`);
87
+ a missing ledger row is a `BlockingGap`. Comments / replies use `N/A`.
79
88
  6. **Establish or refresh the flair-level subreddit baseline** for every target subreddit
80
89
  and assign a `SampleSetID` (see `references/phase4-brief-workflow.md` step B-1.0).
81
90
  优先按 `AssignedFlair` 细分取 exact-flair 样本;记录 `BaselineScope` /
@@ -85,9 +94,12 @@ outline before the structure is decided from evidence.
85
94
  Reuse a valid, unexpired baseline of the same flair coverage; only re-acquire when it is missing or stale.
86
95
  7. **Lock the project diversity window** (`WindowCount` 8–12, `WindowDays` 30–60) if not already locked, and read the rolling window from `memory/phase4-叙事指纹索引.md`.
87
96
  8. Produce board-selection rationale with evidence URLs, then the board overview table.
88
- 9. For every post Key: resolve `客户内容原型` `MappingRuleID`, lock `ContentFunction`, confirm the `EvidenceShape` actually in hand, narrow `StructureFamily` candidates with the ordered filter in `content-type-routing-contract.md`, pre-assign `OpeningMove` / `BrandEntryMode` / `CaveatMode` / `EndingMode` / `LengthBand` / `AnchorSlot`, and the `PostingTrigger`(受控值 `ConcreteFailure` / `PurchaseDecision` / `MeasurementResult` / `CompatibilityGap` / `UnsolicitedTestimony`;`UnsolicitedTestimony` 仅当 `BrandEntryMode: 不出现` 且有真实非征集非品牌控制的 testimony 证据),and **only then** run `RecentCollisionCheck` on the complete triple. On collision, first swap `OpeningMove` / `EndingMode` within the same family; reselect a family only if that fails. **Only then** write the outline.
97
+ 8b. **Run the material-modality preflight (G4, step B-1.4) before locking any structure.** Every selling point / material carries `MaterialID` + `OriginalModality` (`Required` / `Preferred` / `Optional` / `Forbidden`) + `EffectiveModality` + `ModalitySource`. Upgrading modality is blocked unless an `AuthoritativeSource` (`ClientContract` / `SubredditRule` / `ComplianceRequirement` / an already-authorized `ApprovedBrief`) backs it via `ModalityUpgradeRef`, with `ApprovedBy` the generating agent. **A material the user explicitly marked `Optional` is never upgraded** — go back to the user instead. Missing or conflicting modality is fail-closed. Disclosure/compliance requirements are not selling-point modality.
98
+ 9. For every post Key: resolve `客户内容原型` → `MappingRuleID`, lock `ContentFunction`, confirm the `EvidenceShape` actually in hand, narrow `StructureFamily` candidates with the ordered filter in `content-type-routing-contract.md`, pre-assign `OpeningMove` / `CaveatMode` / `EndingMode` / `LengthBand` / `AnchorSlot` (**`BrandEntryMode` is deliberately excluded here — it is assigned in 9b, after its gate**), and **record (not yet validate)** the `PostingTrigger` candidate — its cross-constraint with `BrandEntryMode` can only be checked after 9b(受控值 `ConcreteFailure` / `PurchaseDecision` / `MeasurementResult` / `CompatibilityGap` / `UnsolicitedTestimony`;`UnsolicitedTestimony` 仅当 `BrandEntryMode: 不出现` 且有真实非征集非品牌控制的 testimony 证据),and **only then** — after 9b has supplied `BrandEntryMode` — run `RecentCollisionCheck` on the complete triple. On collision, first swap `OpeningMove` / `EndingMode` within the same family; reselect a family only if that fails. **Only then** write the outline.
99
+ 9b. **Only now assign `BrandEntryMode`, and only through `BrandPlacementGate` (G2):** for **P-B / PGC posts**, placement is decided by the brand-first-mention histogram plus the §2 risk ceiling (`SelectedPlacement` = the later of the two, histogram bucket 结论 mapping to 后段; the domain is 标题/首段/中段/后段, and a class-D board or `NoBrandMentionRate ≥ 0.8` that leaves no viable spot is a `BlockingGap` back to planning, never a silent drop to 不出现/评论) — **never derived from the P-B label**. **P-NB posts** go straight to `不出现` without running the formula; **C-B comments** use `CommentPlacementDecision` plus the §2 ceiling and complement rules. Fill the placement evidence first, **branching by row type**: P-B / PGC posts need a complete `BrandPlacementBasis`; P-NB posts take `N/A(P-NB · 主帖不露品牌)`; C-B comments take `InheritedBrandPlacementBasis` + `CommentPlacementDecision` instead and are never required to carry their own. Legitimate title exposure is preserved for `PGCApproved` (precedent check passed, board allows vendor/official, flair/disclosure/frequency met) and `BrandReviewPost` (genuinely a post about that brand, judged from `PrimaryPostType` / `WritingVariant` plus topic evidence — never by inventing a `ContentFunction: Review` value). This gate governs *what decides placement only* — brand quota semantics (P-B / P-NB / C-B / C-NB / PGC) are untouched.
100
+ 9c. **Narrative layering (G1, step B-1.6) before writing title directions or outlines:** every post and comment carries `InternalCoverageIntent` (FAQ ids, fanout queries, keywords, gap codes, `prompt_id` — internal only) and a non-empty `ReaderVisiblePremise`. The gate scans only reader-facing fields (核心角度 / 标题方向 / 大纲 / 撰写逻辑一句话 / 评论任务方向 / outbound Reddit payload); Routing Decision, TraceFields and 方向选取理由 keep their internal labels. Converting FAQ evidence into a real reader question is the intended outcome — only raw internal labels in the narrative are blocked.
89
101
  10. Create one Brief block for every execution-table Key in the selected batch, including posts, self-comments, and external comments.
90
- 11. For all P-B/C-B/PGC rows, record brand/SKU placement rationale using `references/brand-SKU-PGC-rules.md`.
102
+ 11. For all P-B/C-B/PGC rows, record brand/SKU placement rationale using `references/brand-SKU-PGC-rules.md` (§3's five sample-learning classes are *rationale records only* — they never write `SelectedPlacement` directly nor bypass §2.0's mechanical rules).
91
103
  12. Write a `Planned` fingerprint row for every briefed post Key into `memory/phase4-叙事指纹索引.md` (`DraftHash: PENDING`), including the two `PrewritingControlFields` (`AssignedFlair`, `PostingTrigger`).
92
104
  13. Write or update the memory files listed below, then show the Brief gate checklist.
93
105
 
@@ -110,10 +122,14 @@ Optional Feishu Brief documents require an explicit user-provided folder link.
110
122
  Writing may start only after:
111
123
 
112
124
  - Every target subreddit has a valid, unexpired baseline card with a `SampleSetID`, with flair coverage recorded (`BaselineScope` / `FlairCoverageStatus`); `FlairCoverageStatus` is `Exact` or `ApprovedFallback` (never `Insufficient`).
113
- - Every **main-post** Key carries a non-empty `AssignedFlair` — drawn from the subreddit's live flair list, or the sentinel `NoFlairAvailable` / `NoFlairRequired` for flairless boards — and its `AssignedFlair` matches the baseline card's covered flair (or an approved fallback).
125
+ - Every **main-post** Key carries a non-empty `AssignedFlair` — verified against the ledger's `ProposedFlair` and the subreddit's live flair list (`FlairVerificationStatus: Verified` or `FallbackAssigned`; no `Rejected` left open), or the sentinel `NoFlairAvailable` / `NoFlairRequired` for flairless boards — and its `AssignedFlair` matches the baseline card's covered flair (or an approved fallback).
114
126
  - Every **main-post** Key's `客户内容原型` resolves to an `Approved` mapping row (`MappingRuleID` / `MappingVersion` recorded in the Routing Decision). Self-comments inherit the paired post's value; external comments use `—` and need no mapping of their own.
115
127
  - Every post Brief row carries the nine `LockedFingerprintFields` (`ContentFunction`, `StructureFamily`, `OpeningMove`, `EvidenceShape`, `BrandEntryMode`, `CaveatMode`, `EndingMode`, `LengthBand`, `AnchorSlot`), the two `PrewritingControlFields` (`AssignedFlair`, `PostingTrigger` — the latter satisfying the `PostingTrigger × BrandEntryMode` cross-constraint), and the four `TraceFields` (`SampleSetID`, `RecentCollisionCheck`, `MappingRuleID`, `MappingVersion`).
116
128
  - `RecentCollisionCheck` is `Pass` for every row, or `Exception` with an approved `ExceptionRef`.
129
+ - Every **P-B / PGC post** row carries a complete `BrandPlacementBasis` and a `SelectedPlacement` consistent with it (an already-planned P-B/PGC row may only land on 标题/首段/中段/后段 — moving the brand to a comment or dropping it is a quota change, so it goes back to planning); **C-B comments** carry `InheritedBrandPlacementBasis` + `CommentPlacementDecision` instead and never build their own histogram. No row derives title-brand exposure from the P-B label; `NoComparableEvidence` rows went through re-sampling / StopAndAsk / a recorded `BrandTitleExposureException`, not a default; `Sparse` rows meet the three §2.5 conditions.
130
+ - Every post and comment Brief carries `InternalCoverageIntent` + a non-empty `ReaderVisiblePremise`, and no reader-facing field leaks internal labels.
131
+ - The material-modality ledger is complete, with no unauthorized upgrades or downgrades (and nothing the user marked `Optional` upgraded).
132
+ - Baseline cards cover all **10** dimensions (the 10th is the em-dash / parenthesis usage distribution).
117
133
  - `memory/phase4-叙事指纹索引.md` has exactly one active `Planned` row for every briefed **post** Key (comments are out of scope while `WindowScope: PostOnly`).
118
134
  - `memory/phase4-撰写brief.md` exists and covers every planned Key for the batch.
119
135
  - `brief_approval_mode` is recorded.