@easysociable/cli 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content-engine/scripts/starter.mjs +1 -0
- package/content-engine/starter/CLAUDE.md +105 -42
- package/content-engine/starter/engine.json +1 -0
- package/content-engine/starter/hits/README.md +5 -0
- package/content-engine/starter/hits/_index.md +30 -0
- package/content-engine/starter/hits/_template.md +46 -0
- package/content-engine/starter/hits/entries/README.md +3 -0
- package/content-engine/starter/products/_index.md +1 -0
- package/content-engine/starter/products/entries/P-002-content-engine.md +136 -0
- package/content-engine/starter/profiles/default/README.md +3 -0
- package/content-engine/starter/profiles/default/handles.md +8 -0
- package/content-engine/starter/profiles/default/keywords.md +31 -0
- package/content-engine/starter/runs/_template/brief.md +41 -31
- package/content-engine/starter/topics/_template-item.md +30 -27
- package/content-engine/starter/topics/scoring.md +5 -2
- package/content-engine/workbench/__pycache__/build.cpython-310.pyc +0 -0
- package/content-engine/workbench/build.py +177 -2
- package/content-engine/workbench/serve.py +0 -0
- package/content-engine/workbench/template.html +505 -16
- package/dist/easysociable.js +2 -1
- package/dist/easysociable.js.map +1 -1
- package/package.json +14 -13
- package/skills/easysociable-content/SKILL.md +18 -9
- package/skills/easysociable-content/workflows/NEEDS.md +20 -3
- package/skills/easysociable-template/scripts/generate-theme-from-hex.mjs +0 -0
|
@@ -18,55 +18,58 @@ product_fit: none
|
|
|
18
18
|
recommendation_ids: none
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
#
|
|
21
|
+
# Decision card
|
|
22
22
|
|
|
23
|
-
>
|
|
23
|
+
> A content opportunity under discussion — not a final publish title.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
**状态原因:** 一句话说明为什么得到这个结论
|
|
27
|
-
**适合谁:** 一句话说明具体受众和场景
|
|
28
|
-
**核心判断:** 一句话说明这条内容真正要讨论什么
|
|
29
|
-
**下一步:** 开始生成 / 补需求 / 补证据 / 修改角度 / 放弃
|
|
30
|
-
**产品连接:** none | P-… · direct | adjacent · 一句话说明连接方式
|
|
31
|
-
**生成路径:** demand | demand_to_offer | demand_to_review | review | offer_education | profile_thesis
|
|
25
|
+
**Language:** Match `profiles/<id>/voice.md` **Language** (else pillars “Primary public language”, else `engine.json` locale). Readable judgment + `one_liner` must use that language. Verbatim Needs/Capture quotes stay untranslated. Operator chat may add a short gloss in another language; the file does not.
|
|
32
26
|
|
|
33
|
-
|
|
27
|
+
**Verdict:** ready | backlog | parked
|
|
28
|
+
**Why this status:** one sentence
|
|
29
|
+
**Who it’s for:** one sentence — concrete audience and situation
|
|
30
|
+
**Core judgment:** one sentence — what this piece actually argues
|
|
31
|
+
**Next step:** open a run / fill needs / fill evidence / re-angle / kill
|
|
32
|
+
**Product link:** none | P-… · direct | adjacent · one sentence
|
|
33
|
+
**Generation path:** demand | demand_to_offer | demand_to_review | review | offer_education | profile_thesis
|
|
34
|
+
*(Agent must ask the user to choose before `produce`; do not silent-default.)*
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
## What problem does this solve?
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
2–4 sentences on the real-world problem. Do not restate a title.
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
## What do they believe now?
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
Name the false belief or stuck confusion.
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
## What cut are we making?
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
Our angle only. Not a full headline. Do not rewrite a source post as our thesis.
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
## Why now?
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
What changed, and why this audience must face it now. Not a news summary.
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
## What should change after reading?
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
Before → after for belief or action.
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
## What is this based on?
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
Plain-language Needs, Product, Recommendation, Capture, or other evidence.
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
## Current gaps
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
Missing need evidence, cases, feedback, or product facts. Write `none` if empty.
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
## Suggested form
|
|
65
|
+
|
|
66
|
+
Platform, lane, and form. No full script yet.
|
|
64
67
|
|
|
65
68
|
---
|
|
66
69
|
|
|
67
|
-
##
|
|
70
|
+
## Score / trace appendix
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
For review, machines, and handoff — not the primary reading surface.
|
|
70
73
|
|
|
71
74
|
### Score
|
|
72
75
|
|
|
@@ -185,7 +185,9 @@ raise the audience score.
|
|
|
185
185
|
|
|
186
186
|
### Generation-mode gates
|
|
187
187
|
|
|
188
|
-
Before scoring,
|
|
188
|
+
Before scoring a `produce` Topic, **ask the user to choose `generation_mode`**.
|
|
189
|
+
Recommend `demand` when Need-led; do not silently assume. Then enforce required
|
|
190
|
+
inputs:
|
|
189
191
|
|
|
190
192
|
| mode | Required |
|
|
191
193
|
|------|----------|
|
|
@@ -198,7 +200,8 @@ Before scoring, set the smallest honest `generation_mode`:
|
|
|
198
200
|
|
|
199
201
|
Do not use `review` when the only evidence is vendor marketing. Review entries
|
|
200
202
|
must state limitations, research date, and commercial disclosure where
|
|
201
|
-
relevant. Product-only and recommendation-only inputs do not imply demand
|
|
203
|
+
relevant. Product-only and recommendation-only inputs do not imply demand and
|
|
204
|
+
never raise audience / `ready` alone.
|
|
202
205
|
|
|
203
206
|
When `evidence < 5`, agent should flag an **evidence gap** on the topic item and may suggest **research** before finalizing the score. Topics can be re-scored after research fills the gap.
|
|
204
207
|
|
|
Binary file
|
|
@@ -108,8 +108,13 @@ def extract_tables(md: str) -> list[dict]:
|
|
|
108
108
|
blocks: list[list[str]] = []
|
|
109
109
|
cur: list[str] = []
|
|
110
110
|
for line in md.splitlines():
|
|
111
|
-
|
|
111
|
+
stripped = line.strip()
|
|
112
|
+
if stripped.startswith("|"):
|
|
112
113
|
cur.append(line)
|
|
114
|
+
elif not stripped:
|
|
115
|
+
# Blank lines inside a GFM table must not split the block,
|
|
116
|
+
# or later rows are parsed as a new headerless table.
|
|
117
|
+
continue
|
|
113
118
|
elif cur:
|
|
114
119
|
blocks.append(cur)
|
|
115
120
|
cur = []
|
|
@@ -446,7 +451,7 @@ def build_graph() -> dict:
|
|
|
446
451
|
for p in profiles:
|
|
447
452
|
base = p["path"].rstrip("/") + "/"
|
|
448
453
|
files = {}
|
|
449
|
-
for name in ("README", "voice", "audience", "pillars", "lanes"):
|
|
454
|
+
for name in ("README", "voice", "audience", "pillars", "lanes", "keywords", "handles"):
|
|
450
455
|
rel = f"{base}{name}.md"
|
|
451
456
|
if (ROOT / rel).exists():
|
|
452
457
|
files[name.lower()] = rel
|
|
@@ -562,6 +567,174 @@ def build_graph() -> dict:
|
|
|
562
567
|
)
|
|
563
568
|
# Topic edges are wired after topic nodes exist (see post-pass below).
|
|
564
569
|
|
|
570
|
+
hits_path = ROOT / "hits" / "_index.md"
|
|
571
|
+
if hits_path.exists():
|
|
572
|
+
hits_rows = table_with(
|
|
573
|
+
hits_path.read_text(encoding="utf-8"),
|
|
574
|
+
"id",
|
|
575
|
+
"date",
|
|
576
|
+
"profile",
|
|
577
|
+
"platform",
|
|
578
|
+
"keyword_id",
|
|
579
|
+
"source",
|
|
580
|
+
"hit_kind",
|
|
581
|
+
"signal",
|
|
582
|
+
"status",
|
|
583
|
+
"excerpt",
|
|
584
|
+
"src_likes",
|
|
585
|
+
"src_replies",
|
|
586
|
+
"src_bookmarks",
|
|
587
|
+
"src_views",
|
|
588
|
+
"engagement_at",
|
|
589
|
+
"file",
|
|
590
|
+
)
|
|
591
|
+
if not hits_rows:
|
|
592
|
+
hits_rows = table_with(
|
|
593
|
+
hits_path.read_text(encoding="utf-8"),
|
|
594
|
+
"id",
|
|
595
|
+
"date",
|
|
596
|
+
"profile",
|
|
597
|
+
"platform",
|
|
598
|
+
"source",
|
|
599
|
+
"hit_kind",
|
|
600
|
+
"signal",
|
|
601
|
+
"status",
|
|
602
|
+
"excerpt",
|
|
603
|
+
"file",
|
|
604
|
+
)
|
|
605
|
+
for row in hits_rows:
|
|
606
|
+
ident = row["id"]
|
|
607
|
+
if not ident.startswith("H-"):
|
|
608
|
+
continue
|
|
609
|
+
fpath = wiki_path(row["file"]) or f"hits/entries/{ident}.md"
|
|
610
|
+
if not fpath.endswith(".md"):
|
|
611
|
+
fpath = f"{fpath}.md"
|
|
612
|
+
excerpt = (row.get("excerpt") or "").strip()
|
|
613
|
+
keyword_id = (row.get("keyword_id") or "").strip()
|
|
614
|
+
if not keyword_id:
|
|
615
|
+
m = re.search(r"\bK-\d{2}\b", row.get("source", "") or "")
|
|
616
|
+
if m:
|
|
617
|
+
keyword_id = m.group(0)
|
|
618
|
+
eng = {
|
|
619
|
+
"src_likes": row.get("src_likes", "") or "",
|
|
620
|
+
"src_replies": row.get("src_replies", "") or "",
|
|
621
|
+
"src_bookmarks": row.get("src_bookmarks", "") or "",
|
|
622
|
+
"src_views": row.get("src_views", "") or "",
|
|
623
|
+
"engagement_at": row.get("engagement_at", "") or "",
|
|
624
|
+
}
|
|
625
|
+
source_url = ""
|
|
626
|
+
if fpath and (ROOT / fpath).exists():
|
|
627
|
+
body = read(fpath)
|
|
628
|
+
eng = merge_engagement(eng, parse_engagement_snapshot(body))
|
|
629
|
+
fm = re.match(r"^---\n(.*?)\n---", body, re.S)
|
|
630
|
+
if fm:
|
|
631
|
+
murl = re.search(r"^source_url:\s*(\S+)", fm.group(1), re.M)
|
|
632
|
+
if murl:
|
|
633
|
+
source_url = murl.group(1).strip()
|
|
634
|
+
if not keyword_id:
|
|
635
|
+
mk = re.search(r"^keyword_id:\s*(\S+)", fm.group(1), re.M)
|
|
636
|
+
if mk:
|
|
637
|
+
keyword_id = mk.group(1).strip()
|
|
638
|
+
add_node(
|
|
639
|
+
node(
|
|
640
|
+
"hit",
|
|
641
|
+
ident,
|
|
642
|
+
excerpt or f"Hit {ident}",
|
|
643
|
+
fpath,
|
|
644
|
+
{
|
|
645
|
+
"date": row["date"],
|
|
646
|
+
"profile": row["profile"],
|
|
647
|
+
"platform": row.get("platform", "") or "",
|
|
648
|
+
"keyword_id": keyword_id,
|
|
649
|
+
"source": row.get("source", "") or keyword_id,
|
|
650
|
+
"hit_kind": row.get("hit_kind", "") or "",
|
|
651
|
+
"signal": row.get("signal", "") or "",
|
|
652
|
+
"status": row["status"],
|
|
653
|
+
"excerpt": excerpt,
|
|
654
|
+
"source_url": source_url,
|
|
655
|
+
"src_likes": eng.get("src_likes", "") or "",
|
|
656
|
+
"src_replies": eng.get("src_replies", "") or "",
|
|
657
|
+
"src_bookmarks": eng.get("src_bookmarks", "") or "",
|
|
658
|
+
"src_views": eng.get("src_views", "") or "",
|
|
659
|
+
"engagement_at": eng.get("engagement_at", "") or "",
|
|
660
|
+
"src_heat_note": "来源帖热度,不是你的帖",
|
|
661
|
+
},
|
|
662
|
+
)
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Keyword recipes (profile listening config) + hit counts for Hits rail
|
|
666
|
+
keywords: list[dict] = []
|
|
667
|
+
for p in profiles:
|
|
668
|
+
kpath = ROOT / p["path"].rstrip("/") / "keywords.md"
|
|
669
|
+
if not kpath.exists():
|
|
670
|
+
continue
|
|
671
|
+
krows = table_with(
|
|
672
|
+
kpath.read_text(encoding="utf-8"),
|
|
673
|
+
"id",
|
|
674
|
+
"track",
|
|
675
|
+
"query",
|
|
676
|
+
"platform",
|
|
677
|
+
"intent",
|
|
678
|
+
"layer",
|
|
679
|
+
"pillar",
|
|
680
|
+
"why",
|
|
681
|
+
"weight",
|
|
682
|
+
"status",
|
|
683
|
+
"seed_from",
|
|
684
|
+
"pair_id",
|
|
685
|
+
"volume",
|
|
686
|
+
"kd",
|
|
687
|
+
"serp_intent",
|
|
688
|
+
"volume_source",
|
|
689
|
+
"volume_at",
|
|
690
|
+
"ask_engines",
|
|
691
|
+
"last_scanned",
|
|
692
|
+
)
|
|
693
|
+
if not krows:
|
|
694
|
+
krows = table_with(
|
|
695
|
+
kpath.read_text(encoding="utf-8"),
|
|
696
|
+
"id",
|
|
697
|
+
"query",
|
|
698
|
+
"platform",
|
|
699
|
+
"intent",
|
|
700
|
+
"layer",
|
|
701
|
+
"pillar",
|
|
702
|
+
"why",
|
|
703
|
+
"weight",
|
|
704
|
+
"status",
|
|
705
|
+
"last_scanned",
|
|
706
|
+
)
|
|
707
|
+
for row in krows:
|
|
708
|
+
kid = (row.get("id") or "").strip()
|
|
709
|
+
if not kid.startswith("K-"):
|
|
710
|
+
continue
|
|
711
|
+
track = (row.get("track") or "").strip() or "listen"
|
|
712
|
+
keywords.append(
|
|
713
|
+
{
|
|
714
|
+
"id": kid,
|
|
715
|
+
"profile": p["id"],
|
|
716
|
+
"track": track,
|
|
717
|
+
"query": row.get("query", "") or "",
|
|
718
|
+
"platform": row.get("platform", "") or "",
|
|
719
|
+
"intent": row.get("intent", "") or "",
|
|
720
|
+
"layer": row.get("layer", "") or "",
|
|
721
|
+
"pillar": row.get("pillar", "") or "",
|
|
722
|
+
"why": row.get("why", "") or "",
|
|
723
|
+
"weight": row.get("weight", "") or "",
|
|
724
|
+
"status": row.get("status", "") or "",
|
|
725
|
+
"seed_from": row.get("seed_from", "") or "",
|
|
726
|
+
"pair_id": row.get("pair_id", "") or "",
|
|
727
|
+
"volume": row.get("volume", "") or "",
|
|
728
|
+
"kd": row.get("kd", "") or "",
|
|
729
|
+
"serp_intent": row.get("serp_intent", "") or "",
|
|
730
|
+
"volume_source": row.get("volume_source", "") or "",
|
|
731
|
+
"volume_at": row.get("volume_at", "") or "",
|
|
732
|
+
"ask_engines": row.get("ask_engines", "") or "",
|
|
733
|
+
"last_scanned": row.get("last_scanned", "") or "",
|
|
734
|
+
"path": f"{p['path'].rstrip('/')}/keywords.md",
|
|
735
|
+
}
|
|
736
|
+
)
|
|
737
|
+
|
|
565
738
|
products_path = ROOT / "products" / "_index.md"
|
|
566
739
|
if products_path.exists():
|
|
567
740
|
product_rows = table_with(
|
|
@@ -1240,7 +1413,9 @@ def build_graph() -> dict:
|
|
|
1240
1413
|
"pillars_by_profile": pillars_by_profile,
|
|
1241
1414
|
"pillars": flat_pillars,
|
|
1242
1415
|
"needs_index_present": (ROOT / "needs" / "_index.md").exists(),
|
|
1416
|
+
"hits_index_present": (ROOT / "hits" / "_index.md").exists(),
|
|
1243
1417
|
"products_index_present": (ROOT / "products" / "_index.md").exists(),
|
|
1418
|
+
"keywords": keywords,
|
|
1244
1419
|
"nodes": list(nodes.values()),
|
|
1245
1420
|
"edges": uniq,
|
|
1246
1421
|
}
|
|
File without changes
|