@geoly-ai/social-hub-cli 0.3.17 → 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 (66) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cmd-manifest.json +5 -2
  3. package/dist/cmd-manifest.test.js +3 -0
  4. package/dist/cmd-manifest.test.js.map +1 -1
  5. package/dist/index.d.ts +24 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +74 -9
  8. package/dist/index.js.map +1 -1
  9. package/dist/permissions-gates-admin.d.ts.map +1 -1
  10. package/dist/permissions-gates-admin.js +12 -0
  11. package/dist/permissions-gates-admin.js.map +1 -1
  12. package/dist/register-extensions.d.ts.map +1 -1
  13. package/dist/register-extensions.js +15 -1
  14. package/dist/register-extensions.js.map +1 -1
  15. package/dist/register-feishu-identities.d.ts +47 -0
  16. package/dist/register-feishu-identities.d.ts.map +1 -0
  17. package/dist/register-feishu-identities.js +346 -0
  18. package/dist/register-feishu-identities.js.map +1 -0
  19. package/dist/register-feishu-identities.test.d.ts +2 -0
  20. package/dist/register-feishu-identities.test.d.ts.map +1 -0
  21. package/dist/register-feishu-identities.test.js +124 -0
  22. package/dist/register-feishu-identities.test.js.map +1 -0
  23. package/dist/scrape-output.test.d.ts +2 -0
  24. package/dist/scrape-output.test.d.ts.map +1 -0
  25. package/dist/scrape-output.test.js +58 -0
  26. package/dist/scrape-output.test.js.map +1 -0
  27. package/package.json +3 -3
  28. package/skills/manifest.json +1 -1
  29. package/skills/reddit-content-writing/SKILL.md +1 -1
  30. package/skills/reddit-content-writing/references/phase4-execution-table.md +24 -3
  31. package/skills/reddit-content-writing/references/phase4-writing.md +123 -3
  32. package/skills/reddit-content-writing/references/post-writing-templates.md +14 -6
  33. package/skills/reddit-delivery-check/SKILL.md +1 -0
  34. package/skills/reddit-delivery-check/references/phase5-delivery.md +7 -1
  35. package/skills/reddit-matrix.lock.json +8 -8
  36. package/skills/reddit-phase-brief/SKILL.md +25 -9
  37. package/skills/reddit-phase-brief/references/brand-SKU-PGC-rules.md +223 -36
  38. package/skills/reddit-phase-brief/references/phase4-brief-workflow.md +364 -18
  39. package/skills/reddit-phase-planning/SKILL.md +13 -0
  40. package/skills/reddit-phase-planning/references/phase4-execution-table.md +16 -2
  41. package/skills/reddit-phase-planning/references/phase4-planning.md +74 -15
  42. package/skills/reddit-phase-planning/references/scheduling-decision-procedure.md +386 -0
  43. package/skills/reddit-phase-research/SKILL.md +7 -1
  44. package/skills/reddit-phase-research/references/phase01-research.md +93 -29
  45. package/skills/reddit-strategy-orchestrator/SKILL.md +120 -18
  46. package/skills/reddit-strategy-shared/SKILL.md +4 -2
  47. package/skills/reddit-strategy-shared/references/content-diversity-contract.md +19 -3
  48. package/skills/reddit-strategy-shared/references/context-contract.md +45 -11
  49. package/skills/reddit-strategy-shared/references/evidence-policy.md +139 -0
  50. package/skills/reddit-strategy-shared/references/handoff-schemas.md +16 -1
  51. package/skills/reddit-strategy-shared/references/path-conventions.md +26 -4
  52. package/skills/reddit-strategy-shared/references/prerequisites/README.md +57 -0
  53. package/skills/reddit-strategy-shared/references/prerequisites/{reddit-master-canvas-agent-prompt-template.md → archive/reddit-master-canvas-agent-prompt-template.md} +7 -0
  54. package/skills/reddit-strategy-shared/references/prerequisites/{reddit-sop-v2-universal.md → archive/reddit-sop-v2-universal.md} +7 -0
  55. package/skills/reddit-strategy-shared/references/prerequisites/{reddit-strategy-prerequisites-read.md → archive/reddit-strategy-prerequisites-read.md} +7 -0
  56. package/skills/reddit-strategy-shared/references/prerequisites/{reddit-strategy-reuse-playbook.md → archive/reddit-strategy-reuse-playbook.md} +7 -0
  57. package/skills/reddit-strategy-shared/references/subreddit-research-profile.md +514 -0
  58. package/skills/reddit-strategy-shared/references/version-manifest.md +29 -2
  59. package/skills/reddit-voc-volume/SKILL.md +2 -7
  60. package/skills/reddit-voc-volume/matrix-contract.md +20 -4
  61. package/skills/reddit-voc-volume/references/arctic-shift.md +2 -1
  62. package/skills/reddit-voc-volume/scripts/reddit_voc_volume.py +157 -69
  63. package/skills/reddit-voc-volume/tests/test_archived_comments.py +104 -0
  64. package/skills/social-hub-admin/SKILL.md +47 -1
  65. package/skills/social-hub-cli/SKILL.md +1 -0
  66. package/skills/social-hub-posts/SKILL.md +14 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=register-feishu-identities.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-feishu-identities.test.d.ts","sourceRoot":"","sources":["../src/register-feishu-identities.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { classifyHit, evaluateMassNotFound, } from "./register-feishu-identities.js";
3
+ /**
4
+ * `not_found` 会**清空**已有的 open_id(离职语义)。如果 lark-cli 挂在另一个飞书
5
+ * 租户的授权上,全体查不到,一次 `--apply` 就能抹掉所有映射 —— 而这与「公司集体
6
+ * 离职」在数据上不可区分。这道判据是唯一挡在中间的东西,必须被锁住。
7
+ */
8
+ describe("evaluateMassNotFound", () => {
9
+ const previously = new Set(["u1", "u2", "u3", "u4"]);
10
+ it("全部既有 resolved 变 not_found —— 停机", () => {
11
+ const result = evaluateMassNotFound(previously, [...previously].map((userId) => ({
12
+ userId,
13
+ resolveStatus: "not_found",
14
+ })));
15
+ expect(result).toEqual({ regressions: 4, massNotFound: true });
16
+ });
17
+ it("只掉一个人(真的有人离职)—— 放行", () => {
18
+ const result = evaluateMassNotFound(previously, [
19
+ { userId: "u1", resolveStatus: "not_found" },
20
+ { userId: "u2", resolveStatus: "resolved", openId: "ou_a" },
21
+ { userId: "u3", resolveStatus: "resolved", openId: "ou_b" },
22
+ { userId: "u4", resolveStatus: "resolved", openId: "ou_c" },
23
+ ]);
24
+ expect(result).toEqual({ regressions: 1, massNotFound: false });
25
+ });
26
+ /**
27
+ * `lookup_failed` 不清空 open_id,所以「整批查询失败」不该触发这道闸
28
+ * —— 否则真正危险的信号会被日常噪音淹没。
29
+ */
30
+ it("整批 lookup_failed 不算回归", () => {
31
+ const result = evaluateMassNotFound(previously, [...previously].map((userId) => ({
32
+ userId,
33
+ resolveStatus: "lookup_failed",
34
+ lastErrorCode: "token_expired",
35
+ })));
36
+ expect(result).toEqual({ regressions: 0, massNotFound: false });
37
+ });
38
+ it("此前一个都没映射上时不停机(首次同步)", () => {
39
+ const result = evaluateMassNotFound(new Set(), [
40
+ { userId: "u1", resolveStatus: "not_found" },
41
+ { userId: "u2", resolveStatus: "not_found" },
42
+ ]);
43
+ expect(result).toEqual({ regressions: 0, massNotFound: false });
44
+ });
45
+ it("恰好一半不触发,过半才触发(边界)", () => {
46
+ const half = evaluateMassNotFound(new Set(["u1", "u2"]), [
47
+ { userId: "u1", resolveStatus: "not_found" },
48
+ { userId: "u2", resolveStatus: "resolved", openId: "ou_a" },
49
+ ]);
50
+ expect(half.massNotFound).toBe(false);
51
+ const overHalf = evaluateMassNotFound(new Set(["u1", "u2", "u3"]), [
52
+ { userId: "u1", resolveStatus: "not_found" },
53
+ { userId: "u2", resolveStatus: "not_found" },
54
+ { userId: "u3", resolveStatus: "resolved", openId: "ou_a" },
55
+ ]);
56
+ expect(overHalf.massNotFound).toBe(true);
57
+ });
58
+ it("不在既有 resolved 名单里的 not_found 不算回归(本来就没映射)", () => {
59
+ const result = evaluateMassNotFound(new Set(["u1"]), [
60
+ { userId: "u9", resolveStatus: "not_found" },
61
+ { userId: "u8", resolveStatus: "not_found" },
62
+ { userId: "u1", resolveStatus: "resolved", openId: "ou_a" },
63
+ ]);
64
+ expect(result).toEqual({ regressions: 0, massNotFound: false });
65
+ });
66
+ });
67
+ /**
68
+ * 分类判据。只有「飞书压根没返回这个邮箱」才允许写 `not_found`
69
+ * —— 那一条会**清空**既有 open_id。其余"没拿到可用答案"必须是 `lookup_failed`。
70
+ */
71
+ describe("classifyHit", () => {
72
+ it("飞书静默省略该邮箱 → not_found(真的不在通讯录)", () => {
73
+ expect(classifyHit("u1", undefined)).toEqual({
74
+ userId: "u1",
75
+ resolveStatus: "not_found",
76
+ });
77
+ });
78
+ it("命中了人但没有 open_id → lookup_failed,不清空既有映射", () => {
79
+ expect(classifyHit("u1", {})).toEqual({
80
+ userId: "u1",
81
+ resolveStatus: "lookup_failed",
82
+ lastErrorCode: "missing_open_id",
83
+ });
84
+ });
85
+ it("一个邮箱命中多个不同的人 → lookup_failed(歧义,不能挑一个)", () => {
86
+ expect(classifyHit("u1", { ambiguous: true })).toEqual({
87
+ userId: "u1",
88
+ resolveStatus: "lookup_failed",
89
+ lastErrorCode: "ambiguous_match",
90
+ });
91
+ });
92
+ it.each([
93
+ ["注入串", "ou_x></at><at user_id=all"],
94
+ ["缺前缀", "abc123"],
95
+ ["带引号", 'ou_abc"'],
96
+ ])("open_id 畸形(%s) → lookup_failed 而不是写进库", (_label, openId) => {
97
+ expect(classifyHit("u1", { openId })).toMatchObject({
98
+ resolveStatus: "lookup_failed",
99
+ lastErrorCode: "malformed_open_id",
100
+ });
101
+ });
102
+ it("union_id 畸形 → lookup_failed(不退化成只写 open_id)", () => {
103
+ expect(classifyHit("u1", { openId: "ou_ok1", unionId: "on_x></at>" })).toMatchObject({
104
+ resolveStatus: "lookup_failed",
105
+ lastErrorCode: "malformed_union_id",
106
+ });
107
+ });
108
+ it("合法命中 → resolved", () => {
109
+ expect(classifyHit("u1", { openId: "ou_ok1", unionId: "on_ok1" })).toEqual({
110
+ userId: "u1",
111
+ resolveStatus: "resolved",
112
+ openId: "ou_ok1",
113
+ unionId: "on_ok1",
114
+ });
115
+ });
116
+ it("没有 union_id 也能 resolved(该字段可缺)", () => {
117
+ expect(classifyHit("u1", { openId: "ou_ok1" })).toEqual({
118
+ userId: "u1",
119
+ resolveStatus: "resolved",
120
+ openId: "ou_ok1",
121
+ });
122
+ });
123
+ });
124
+ //# sourceMappingURL=register-feishu-identities.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-feishu-identities.test.js","sourceRoot":"","sources":["../src/register-feishu-identities.test.ts"],"names":[],"mappings":"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"}
@@ -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.17",
3
+ "version": "0.3.19",
4
4
  "type": "module",
5
5
  "description": "social-hub CLI for Social Ops Hub",
6
6
  "repository": {
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "commander": "^12.1.0",
25
- "@geoly-ai/social-hub-authz": "0.0.14",
26
- "@geoly-ai/social-hub-sdk": "0.0.54"
25
+ "@geoly-ai/social-hub-authz": "0.0.17",
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.17",
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": "45332556a0835a5c16386cb31550c04ea2447e2326a7a4ff15fd050f1ae91d55"
31
+ "contentSha256": "496c0d57af7507d86a16bd4d06f3a5a017ca10b3dc4e3564d82b0c988e1c87ac"
32
32
  },
33
33
  "reddit-brand-risk-response": {
34
34
  "contentSha256": "dbba80345ebe68818a25e7604dfaa6951c6bf0e4d785d5013be7a06b22ec5b7c"