@easysociable/cli 0.1.2 → 0.1.4
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/README.md +32 -21
- package/{skills/easysociable-content → content-engine}/scripts/starter.mjs +3 -1
- package/{skills/easysociable-content → content-engine}/scripts/workbench.mjs +13 -8
- package/{skills/easysociable-content → content-engine}/starter/CLAUDE.md +217 -128
- package/{skills/easysociable-content → content-engine}/starter/engine.json +3 -0
- package/content-engine/starter/needs/README.md +38 -0
- package/content-engine/starter/needs/_index.md +31 -0
- package/content-engine/starter/needs/_template.md +24 -0
- package/content-engine/starter/needs/entries/README.md +5 -0
- package/content-engine/starter/products/README.md +38 -0
- package/content-engine/starter/products/_index.md +24 -0
- package/content-engine/starter/products/_template.md +56 -0
- package/content-engine/starter/products/entries/P-001-easysociable.md +150 -0
- package/content-engine/starter/profiles/_index.md +16 -0
- package/{skills/easysociable-content → content-engine}/starter/profiles/default/README.md +4 -1
- package/content-engine/starter/profiles/default/boundaries.md +14 -0
- package/content-engine/starter/profiles/default/expression.md +18 -0
- package/content-engine/starter/profiles/default/lanes.md +12 -0
- package/content-engine/starter/profiles/default/stories.md +15 -0
- package/{skills/easysociable-content → content-engine}/starter/profiles/default/voice.md +14 -0
- package/content-engine/starter/recommendations/README.md +21 -0
- package/content-engine/starter/recommendations/_index.md +21 -0
- package/content-engine/starter/recommendations/_template.md +44 -0
- package/{skills/easysociable-content → content-engine}/starter/runs/_template/brief.md +1 -1
- package/{skills/easysociable-content → content-engine}/starter/runs/_template/pack.md +15 -7
- package/{skills/easysociable-content → content-engine}/starter/runs/_template/packet.md +6 -0
- package/{skills/easysociable-content → content-engine}/starter/swipe/_template.md +3 -1
- package/content-engine/starter/topics/README.md +21 -0
- package/{skills/easysociable-content → content-engine}/starter/topics/_index.md +12 -5
- package/content-engine/starter/topics/_template-item.md +103 -0
- package/{skills/easysociable-content → content-engine}/starter/topics/scoring.md +55 -4
- package/content-engine/workbench/__pycache__/build.cpython-310.pyc +0 -0
- package/{skills/easysociable-content → content-engine}/workbench/build.py +306 -97
- package/{skills/easysociable-content → content-engine}/workbench/serve.py +0 -0
- package/{skills/easysociable-content → content-engine}/workbench/template.html +173 -27
- package/dist/easysociable.js +629 -142
- package/dist/easysociable.js.map +1 -1
- package/package.json +15 -15
- package/skills/ONBOARDING.md +21 -18
- package/skills/easysociable-content/SKILL.md +104 -98
- package/skills/easysociable-content/workflows/INTERVIEW.md +70 -0
- package/skills/easysociable-content/workflows/NEEDS.md +82 -0
- package/skills/easysociable-slideshow/SKILL.md +79 -44
- package/skills/easysociable-slideshow/references/ARC.md +44 -0
- package/skills/easysociable-slideshow/references/CONTENT_PACK.md +34 -6
- package/skills/easysociable-slideshow/workflows/CREATE_RUN.md +24 -5
- package/skills/easysociable-slideshow/workflows/OUT_OF_SCOPE.md +3 -0
- package/skills/easysociable-template/SKILL.md +29 -30
- package/skills/easysociable-template/references/CATALOG_METADATA.md +23 -20
- package/skills/easysociable-template/references/DESIGN_V2_1.md +53 -2
- package/skills/easysociable-template/references/GATEWAY.md +7 -2
- package/skills/easysociable-template/references/PLATFORM_SPECS.md +11 -3
- package/skills/easysociable-template/references/THEME_TOKENS.md +1 -1
- package/skills/easysociable-template/references/fixtures/image-reference.design-plan.json +74 -0
- package/skills/easysociable-template/references/fixtures/image-reference.visual-qa.json +28 -0
- package/skills/easysociable-template/scripts/generate-theme-from-hex.mjs +0 -0
- package/skills/easysociable-template/scripts/validate-design-v2.mjs +97 -0
- package/skills/easysociable-template/workflows/ASSET_PLAN.md +2 -1
- package/skills/easysociable-template/workflows/AUTHORING_FROM_BRIEF.md +4 -4
- package/skills/easysociable-template/workflows/AUTHORING_FROM_IMAGE.md +66 -16
- package/skills/easysociable-template/workflows/AUTHORING_STUDIO.md +28 -15
- package/skills/easysociable-template/workflows/DESIGN_BRIEF.md +4 -3
- package/skills/easysociable-template/workflows/FILL_AND_RENDER.md +4 -83
- package/skills/easysociable-template/workflows/MULTI_PLATFORM_FAMILY.md +81 -0
- package/skills/easysociable-template/workflows/OUT_OF_SCOPE.md +3 -2
- package/skills/easysociable-template/workflows/PUBLISH_FROM_DRAFT.md +1 -1
- package/skills/easysociable-template/workflows/VISUAL_QA.md +10 -0
- package/skills/easysociable-content/INSTALL.md +0 -60
- package/skills/easysociable-content/platforms/claude.md +0 -4
- package/skills/easysociable-content/platforms/codex.md +0 -4
- package/skills/easysociable-content/platforms/hermes.md +0 -4
- package/skills/easysociable-content/platforms/openclaw.md +0 -4
- package/skills/easysociable-content/references/CLI.md +0 -40
- package/skills/easysociable-content/references/CONTRACT_SYNC.md +0 -42
- package/skills/easysociable-content/references/MCP.md +0 -15
- package/skills/easysociable-content/references/PAGE_PACK.md +0 -129
- package/skills/easysociable-content/references/PLATFORM_GUIDE.md +0 -36
- package/skills/easysociable-content/references/RUN_LAYOUT.md +0 -73
- package/skills/easysociable-content/references/TROUBLESHOOTING.md +0 -13
- package/skills/easysociable-content/references/VAULT.md +0 -40
- package/skills/easysociable-content/references/VOICE.md +0 -16
- package/skills/easysociable-content/scripts/package-skill.mjs +0 -49
- package/skills/easysociable-content/scripts/setup.mjs +0 -200
- package/skills/easysociable-content/scripts/setup.test.mjs +0 -82
- package/skills/easysociable-content/starter/discovery/README.md +0 -18
- package/skills/easysociable-content/starter/discovery/_index.md +0 -27
- package/skills/easysociable-content/starter/discovery/_template.md +0 -41
- package/skills/easysociable-content/starter/profiles/_index.md +0 -13
- package/skills/easysociable-content/starter/topics/README.md +0 -25
- package/skills/easysociable-content/starter/topics/WATCHLIST.md +0 -65
- package/skills/easysociable-content/starter/topics/_template-item.md +0 -90
- package/skills/easysociable-content/starter/topics/watchlists/README.md +0 -10
- package/skills/easysociable-content/starter/topics/watchlists/_shared.md +0 -14
- package/skills/easysociable-content/starter/topics/watchlists/default.md +0 -6
- package/skills/easysociable-content/workflows/ANALYSIS_AND_COVERAGE.md +0 -17
- package/skills/easysociable-content/workflows/CONTENT_EXPORT.md +0 -24
- package/skills/easysociable-content/workflows/GETTING_STARTED.md +0 -40
- package/skills/easysociable-content/workflows/MCP_FIRST.md +0 -5
- package/skills/easysociable-content/workflows/OUT_OF_SCOPE.md +0 -18
- package/skills/easysociable-content/workflows/PACK_AUTHORING.md +0 -42
- package/skills/easysociable-content/workflows/SWIPE_CAPTURE.md +0 -18
- package/skills/easysociable-content/workflows/TOPIC_DRAFT.md +0 -29
- package/skills/easysociable-content/workflows/WORKING_DRAFT.md +0 -19
- /package/{skills/easysociable-content → content-engine}/fixtures/golden-run-en/draft.md +0 -0
- /package/{skills/easysociable-content → content-engine}/fixtures/golden-run-en/idea.md +0 -0
- /package/{skills/easysociable-content → content-engine}/fixtures/golden-run-en/pack-tiktok.json +0 -0
- /package/{skills/easysociable-content → content-engine}/scripts/contracts.mjs +0 -0
- /package/{skills/easysociable-content → content-engine}/scripts/export-contracts.mjs +0 -0
- /package/{skills/easysociable-content → content-engine}/scripts/fixture.test.mjs +0 -0
- /package/{skills/easysociable-content → content-engine}/scripts/validate-export.mjs +0 -0
- /package/{skills/easysociable-content → content-engine}/scripts/validate-pack.mjs +0 -0
- /package/{skills/easysociable-content → content-engine}/scripts/validate-run.mjs +0 -0
- /package/{skills/easysociable-content → content-engine}/scripts/validate.test.mjs +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/accounts/README.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/accounts/_index.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/accounts/overrides/README.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/exports/README.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/inbox/README.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/inbox/_index.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/inbox/entries/README.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/platforms/README.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/platforms/ig.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/platforms/instagram.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/platforms/linkedin.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/platforms/threads.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/platforms/tiktok.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/platforms/x.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/platforms/xiaohongshu.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/profiles/default/audience.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/profiles/default/pillars.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/published/_index.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/raw/README.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/raw/_index.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/runs/README.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/runs/_index.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/runs/_template/draft.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/runs/_template/editor.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/runs/_template/feedback.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/runs/_template/idea.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/runs/_template/rubric.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/swipe/_index.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/topics/items/README.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/wiki/README.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/wiki/atoms/_index.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/wiki/atoms/_template.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/wiki/claims/_index.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/wiki/claims/_template.md +0 -0
- /package/{skills/easysociable-content → content-engine}/starter/workbench/README.md +0 -0
|
@@ -29,6 +29,8 @@ SWIPE_RE = re.compile(r"\bS\d+\b")
|
|
|
29
29
|
ATOM_RE = re.compile(r"\bA-[a-z0-9-]+\b")
|
|
30
30
|
CLAIM_RE = re.compile(r"\bC-(?!\d{8})[a-z0-9-]+\b")
|
|
31
31
|
RUN_RE = re.compile(r"RUN-[A-Za-z0-9-]+")
|
|
32
|
+
PRODUCT_RE = re.compile(r"\bP-[A-Za-z0-9-]+\b")
|
|
33
|
+
RECOMMENDATION_RE = re.compile(r"\bR-[A-Za-z0-9-]+\b")
|
|
32
34
|
WIKI = re.compile(r"\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]")
|
|
33
35
|
|
|
34
36
|
|
|
@@ -165,7 +167,8 @@ HTTP_RE = re.compile(r"https?://[^\s\]\)>'\"|]+")
|
|
|
165
167
|
def wiki_path(cell: str) -> str:
|
|
166
168
|
m = WIKI.search(cell or "")
|
|
167
169
|
if not m:
|
|
168
|
-
|
|
170
|
+
raw = (cell or "").strip().strip("`")
|
|
171
|
+
return raw if raw.endswith(".md") and "://" not in raw else ""
|
|
169
172
|
p = m.group(1).strip()
|
|
170
173
|
if p.endswith(".md"):
|
|
171
174
|
return p
|
|
@@ -304,44 +307,128 @@ def parse_feedback_metrics(text: str) -> dict:
|
|
|
304
307
|
|
|
305
308
|
|
|
306
309
|
ENGAGEMENT_SNAP_RE = re.compile(
|
|
307
|
-
r"engagement\s+snapshot
|
|
310
|
+
r"(?:engagement\s+snapshot|snapshot\s+engagement)[^\n:]*:\s*(.+?)(?:\n|$)",
|
|
311
|
+
re.I,
|
|
312
|
+
)
|
|
313
|
+
ENGAGEMENT_SECTION_RE = re.compile(
|
|
314
|
+
r"^##\s+(?:Engagement\s+snapshot|Snapshot\s+metrics(?:\s*\([^)]*\))?)\s*\n+(.*?)(?=^##\s|\Z)",
|
|
315
|
+
re.I | re.M | re.S,
|
|
316
|
+
)
|
|
317
|
+
ENGAGEMENT_KIND = r"likes?|replies|reply|bookmarks?|views?"
|
|
318
|
+
ENGAGEMENT_NUM = r"~?\d[\d,]*(?:\.\d+)?[kKmMbB]?"
|
|
319
|
+
# kind-first: likes 113 / likes: 3
|
|
320
|
+
ENGAGEMENT_KIND_NUM_RE = re.compile(
|
|
321
|
+
rf"\b({ENGAGEMENT_KIND})\s*[:=]?\s*({ENGAGEMENT_NUM})\b",
|
|
308
322
|
re.I,
|
|
309
323
|
)
|
|
310
|
-
|
|
311
|
-
|
|
324
|
+
# num-first: 1086 likes / ~92.9k views
|
|
325
|
+
ENGAGEMENT_NUM_KIND_RE = re.compile(
|
|
326
|
+
rf"({ENGAGEMENT_NUM})\s+({ENGAGEMENT_KIND})\b",
|
|
312
327
|
re.I,
|
|
313
328
|
)
|
|
314
329
|
ENGAGEMENT_AT_RE = re.compile(r"@\s*(\d{4}-\d{2}-\d{2})")
|
|
330
|
+
ENGAGEMENT_FETCH_AT_RE = re.compile(
|
|
331
|
+
r"(?:fetch|at\s+capture|snapshot_at)\s*[:=]?\s*(\d{4}-\d{2}-\d{2})",
|
|
332
|
+
re.I,
|
|
333
|
+
)
|
|
315
334
|
|
|
316
335
|
|
|
317
|
-
def
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
Only the primary snapshot counts. Parenthetical notes like
|
|
321
|
-
`(quoted earlier demo: likes 67 / bookmarks 115)` are ignored.
|
|
322
|
-
"""
|
|
323
|
-
out = {
|
|
336
|
+
def _empty_engagement() -> dict[str, str]:
|
|
337
|
+
return {
|
|
324
338
|
"src_likes": "",
|
|
325
339
|
"src_replies": "",
|
|
326
340
|
"src_bookmarks": "",
|
|
327
341
|
"src_views": "",
|
|
328
342
|
"engagement_at": "",
|
|
329
343
|
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _normalize_metric_kind(kind: str) -> str:
|
|
347
|
+
k = kind.lower()
|
|
348
|
+
if k.startswith("like"):
|
|
349
|
+
return "likes"
|
|
350
|
+
if k.startswith("repl"):
|
|
351
|
+
return "replies"
|
|
352
|
+
if k.startswith("bookmark"):
|
|
353
|
+
return "bookmarks"
|
|
354
|
+
if k.startswith("view"):
|
|
355
|
+
return "views"
|
|
356
|
+
return k
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _parse_compact_count(raw: str) -> str:
|
|
360
|
+
"""'92.9k' → '92900'; '1.1M' → '1100000'; '9,574' → '9574'."""
|
|
361
|
+
s = raw.strip().lstrip("~").replace(",", "")
|
|
362
|
+
if not s:
|
|
363
|
+
return ""
|
|
364
|
+
m = re.fullmatch(r"(\d+(?:\.\d+)?)([kKmMbB])?", s)
|
|
365
|
+
if not m:
|
|
366
|
+
digits = re.sub(r"[^\d]", "", s)
|
|
367
|
+
return digits
|
|
368
|
+
n = float(m.group(1))
|
|
369
|
+
suf = (m.group(2) or "").lower()
|
|
370
|
+
if suf == "k":
|
|
371
|
+
n *= 1_000
|
|
372
|
+
elif suf == "m":
|
|
373
|
+
n *= 1_000_000
|
|
374
|
+
elif suf == "b":
|
|
375
|
+
n *= 1_000_000_000
|
|
376
|
+
return str(int(round(n)))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _fill_engagement_from_blob(out: dict[str, str], blob: str) -> None:
|
|
380
|
+
if not blob:
|
|
381
|
+
return
|
|
382
|
+
# Prefer kind-first matches (discovery + list form), then num-first (capture prose).
|
|
383
|
+
for kind, num in ENGAGEMENT_KIND_NUM_RE.findall(blob):
|
|
384
|
+
key = f"src_{_normalize_metric_kind(kind)}"
|
|
385
|
+
if key in out and not out[key]:
|
|
386
|
+
out[key] = _parse_compact_count(num)
|
|
387
|
+
for num, kind in ENGAGEMENT_NUM_KIND_RE.findall(blob):
|
|
388
|
+
key = f"src_{_normalize_metric_kind(kind)}"
|
|
389
|
+
if key in out and not out[key]:
|
|
390
|
+
out[key] = _parse_compact_count(num)
|
|
391
|
+
if not out["engagement_at"]:
|
|
392
|
+
at = ENGAGEMENT_AT_RE.search(blob) or ENGAGEMENT_FETCH_AT_RE.search(blob)
|
|
393
|
+
if at:
|
|
394
|
+
out["engagement_at"] = at.group(1)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def parse_engagement_snapshot(text: str) -> dict[str, str]:
|
|
398
|
+
"""Parse engagement into src_* fields from Discovery or Capture/Raw notes.
|
|
399
|
+
|
|
400
|
+
Supports:
|
|
401
|
+
- Discovery: `engagement snapshot: likes 113 / replies 18 / ... @ 2026-08-30`
|
|
402
|
+
- Capture prose: `Engagement snapshot (fetch DATE): ~92.9k views · 1086 likes · ...`
|
|
403
|
+
- Raw list under `## Engagement snapshot` with `- likes: 3`
|
|
404
|
+
Parenthetical secondary demos after the primary snapshot are ignored.
|
|
405
|
+
"""
|
|
406
|
+
out = _empty_engagement()
|
|
330
407
|
if not text:
|
|
331
408
|
return out
|
|
332
409
|
m = ENGAGEMENT_SNAP_RE.search(text)
|
|
333
|
-
if
|
|
410
|
+
if m:
|
|
411
|
+
blob = m.group(1).split("(", 1)[0]
|
|
412
|
+
# Date often sits in the label: Engagement snapshot (fetch 2026-08-25)
|
|
413
|
+
label = text[m.start() : m.start(1)]
|
|
414
|
+
_fill_engagement_from_blob(out, blob)
|
|
415
|
+
if not out["engagement_at"]:
|
|
416
|
+
at = ENGAGEMENT_FETCH_AT_RE.search(label) or ENGAGEMENT_AT_RE.search(label)
|
|
417
|
+
if at:
|
|
418
|
+
out["engagement_at"] = at.group(1)
|
|
334
419
|
return out
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
420
|
+
sec = ENGAGEMENT_SECTION_RE.search(text)
|
|
421
|
+
if sec:
|
|
422
|
+
_fill_engagement_from_blob(out, sec.group(1))
|
|
423
|
+
return out
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def merge_engagement(*parts: dict[str, str]) -> dict[str, str]:
|
|
427
|
+
out = _empty_engagement()
|
|
428
|
+
for part in parts:
|
|
429
|
+
for k, v in part.items():
|
|
430
|
+
if k in out and v and not out[k]:
|
|
431
|
+
out[k] = v
|
|
345
432
|
return out
|
|
346
433
|
|
|
347
434
|
def build_graph() -> dict:
|
|
@@ -359,7 +446,7 @@ def build_graph() -> dict:
|
|
|
359
446
|
for p in profiles:
|
|
360
447
|
base = p["path"].rstrip("/") + "/"
|
|
361
448
|
files = {}
|
|
362
|
-
for name in ("README", "voice", "audience", "pillars"):
|
|
449
|
+
for name in ("README", "voice", "audience", "pillars", "lanes"):
|
|
363
450
|
rel = f"{base}{name}.md"
|
|
364
451
|
if (ROOT / rel).exists():
|
|
365
452
|
files[name.lower()] = rel
|
|
@@ -432,102 +519,142 @@ def build_graph() -> dict:
|
|
|
432
519
|
for kind, lib_id in parse_library(row["library"]):
|
|
433
520
|
edges.append({"from": nid("capture", ident), "to": nid(kind, lib_id), "rel": "ingested_as"})
|
|
434
521
|
|
|
435
|
-
|
|
522
|
+
needs_md = read("needs/_index.md")
|
|
523
|
+
needs_rows = table_with(
|
|
524
|
+
needs_md,
|
|
525
|
+
"id",
|
|
526
|
+
"date",
|
|
527
|
+
"profile",
|
|
528
|
+
"source_type",
|
|
529
|
+
"quote",
|
|
530
|
+
"speaker",
|
|
531
|
+
"frequency",
|
|
532
|
+
"status",
|
|
533
|
+
"topic_ids",
|
|
534
|
+
"file",
|
|
535
|
+
)
|
|
536
|
+
for row in needs_rows:
|
|
436
537
|
ident = row["id"]
|
|
437
|
-
|
|
438
|
-
|
|
538
|
+
if not ident.startswith("N-"):
|
|
539
|
+
continue
|
|
540
|
+
fpath = wiki_path(row["file"]) or f"needs/entries/{ident}.md"
|
|
541
|
+
if not fpath.endswith(".md"):
|
|
439
542
|
fpath = f"{fpath}.md"
|
|
543
|
+
topic_ids = (row.get("topic_ids") or "").strip()
|
|
544
|
+
quote = (row.get("quote") or "").strip()
|
|
440
545
|
add_node(
|
|
441
546
|
node(
|
|
442
|
-
"
|
|
547
|
+
"need",
|
|
443
548
|
ident,
|
|
444
|
-
|
|
549
|
+
quote or f"Need {ident}",
|
|
445
550
|
fpath,
|
|
446
551
|
{
|
|
447
552
|
"date": row["date"],
|
|
448
|
-
"
|
|
449
|
-
"
|
|
450
|
-
"
|
|
553
|
+
"profile": row["profile"],
|
|
554
|
+
"source_type": row["source_type"],
|
|
555
|
+
"quote": quote,
|
|
556
|
+
"speaker": row.get("speaker", "") or "",
|
|
557
|
+
"frequency": row.get("frequency", "") or "1",
|
|
558
|
+
"status": row["status"],
|
|
559
|
+
"topic_ids": topic_ids,
|
|
451
560
|
},
|
|
452
561
|
)
|
|
453
562
|
)
|
|
454
|
-
|
|
455
|
-
edge(nid("capture", row["capture_id"]), nid("raw", ident), "has_raw")
|
|
563
|
+
# Topic edges are wired after topic nodes exist (see post-pass below).
|
|
456
564
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
"id",
|
|
462
|
-
"date",
|
|
463
|
-
"profile",
|
|
464
|
-
"platform",
|
|
465
|
-
"source_handle",
|
|
466
|
-
"source_url",
|
|
467
|
-
"source_claim",
|
|
468
|
-
"source_role",
|
|
469
|
-
"status",
|
|
470
|
-
"next_action",
|
|
471
|
-
"topic_ids",
|
|
472
|
-
"file",
|
|
473
|
-
)
|
|
474
|
-
if not discovery_rows:
|
|
475
|
-
discovery_rows = table_with(
|
|
476
|
-
discovery_md,
|
|
565
|
+
products_path = ROOT / "products" / "_index.md"
|
|
566
|
+
if products_path.exists():
|
|
567
|
+
product_rows = table_with(
|
|
568
|
+
products_path.read_text(encoding="utf-8"),
|
|
477
569
|
"id",
|
|
478
|
-
"
|
|
479
|
-
"
|
|
480
|
-
"
|
|
481
|
-
"
|
|
482
|
-
"
|
|
483
|
-
"
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
"
|
|
487
|
-
"
|
|
570
|
+
"name",
|
|
571
|
+
"status",
|
|
572
|
+
"audience",
|
|
573
|
+
"price",
|
|
574
|
+
"effective_from",
|
|
575
|
+
"file",
|
|
576
|
+
)
|
|
577
|
+
for row in product_rows:
|
|
578
|
+
ident = row["id"]
|
|
579
|
+
if not ident.startswith("P-"):
|
|
580
|
+
continue
|
|
581
|
+
fpath = wiki_path(row["file"]) or f"products/entries/{ident}.md"
|
|
582
|
+
if not fpath.endswith(".md"):
|
|
583
|
+
fpath = f"{fpath}.md"
|
|
584
|
+
add_node(
|
|
585
|
+
node(
|
|
586
|
+
"product",
|
|
587
|
+
ident,
|
|
588
|
+
row["name"] or ident,
|
|
589
|
+
fpath,
|
|
590
|
+
{
|
|
591
|
+
"status": row["status"],
|
|
592
|
+
"audience": row["audience"],
|
|
593
|
+
"price": row["price"],
|
|
594
|
+
"effective_from": row["effective_from"],
|
|
595
|
+
},
|
|
596
|
+
)
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
recommendations_path = ROOT / "recommendations" / "_index.md"
|
|
600
|
+
if recommendations_path.exists():
|
|
601
|
+
recommendation_rows = table_with(
|
|
602
|
+
recommendations_path.read_text(encoding="utf-8"),
|
|
603
|
+
"id",
|
|
604
|
+
"name",
|
|
488
605
|
"status",
|
|
489
|
-
"
|
|
490
|
-
"
|
|
491
|
-
"
|
|
606
|
+
"category",
|
|
607
|
+
"profiles",
|
|
608
|
+
"pillars",
|
|
609
|
+
"last_researched",
|
|
610
|
+
"relationship",
|
|
492
611
|
"file",
|
|
493
612
|
)
|
|
494
|
-
|
|
613
|
+
for row in recommendation_rows:
|
|
614
|
+
ident = row["id"]
|
|
615
|
+
if not ident.startswith("R-"):
|
|
616
|
+
continue
|
|
617
|
+
fpath = wiki_path(row["file"]) or f"recommendations/{ident}.md"
|
|
618
|
+
if not fpath.endswith(".md"):
|
|
619
|
+
fpath = f"{fpath}.md"
|
|
620
|
+
add_node(
|
|
621
|
+
node(
|
|
622
|
+
"recommendation",
|
|
623
|
+
ident,
|
|
624
|
+
row["name"] or ident,
|
|
625
|
+
fpath,
|
|
626
|
+
{
|
|
627
|
+
"status": row["status"],
|
|
628
|
+
"category": row["category"],
|
|
629
|
+
"profiles": row["profiles"],
|
|
630
|
+
"pillars": row["pillars"],
|
|
631
|
+
"last_researched": row["last_researched"],
|
|
632
|
+
"relationship": row["relationship"],
|
|
633
|
+
},
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
for row in table_with(read("raw/_index.md"), "id", "date", "capture_id", "file", "type", "source_url", "one_liner"):
|
|
495
638
|
ident = row["id"]
|
|
496
|
-
if not ident.startswith("D-"):
|
|
497
|
-
continue
|
|
498
639
|
fpath = wiki_path(row["file"])
|
|
499
640
|
if fpath and not fpath.endswith(".md"):
|
|
500
641
|
fpath = f"{fpath}.md"
|
|
501
|
-
topic_ids = (row.get("topic_ids") or "").strip()
|
|
502
|
-
eng = parse_engagement_snapshot(read(fpath) if fpath and (ROOT / fpath).exists() else "")
|
|
503
642
|
add_node(
|
|
504
643
|
node(
|
|
505
|
-
"
|
|
644
|
+
"raw",
|
|
506
645
|
ident,
|
|
507
|
-
row["
|
|
646
|
+
row["one_liner"],
|
|
508
647
|
fpath,
|
|
509
648
|
{
|
|
510
649
|
"date": row["date"],
|
|
511
|
-
"
|
|
512
|
-
"platform": row["platform"],
|
|
513
|
-
"source_handle": row["source_handle"],
|
|
650
|
+
"type": row["type"],
|
|
514
651
|
"source_url": row["source_url"],
|
|
515
|
-
"
|
|
516
|
-
"source_role": row["source_role"],
|
|
517
|
-
"relevance": row.get("relevance", "") or "",
|
|
518
|
-
"ownership": row.get("ownership", "") or "",
|
|
519
|
-
"product_relevance": row.get("product_relevance", "") or "",
|
|
520
|
-
"status": row["status"],
|
|
521
|
-
"next_action": row.get("next_action", "") or "",
|
|
522
|
-
"candidates": row.get("candidates", "") or "",
|
|
523
|
-
"topic_ids": topic_ids,
|
|
524
|
-
**eng,
|
|
652
|
+
"capture_id": row["capture_id"],
|
|
525
653
|
},
|
|
526
654
|
)
|
|
527
655
|
)
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
edge(nid("discovery", ident), nid("topic", tid), "candidate")
|
|
656
|
+
if row["capture_id"]:
|
|
657
|
+
edge(nid("capture", row["capture_id"]), nid("raw", ident), "has_raw")
|
|
531
658
|
|
|
532
659
|
topic_md = read("topics/_index.md")
|
|
533
660
|
# Current: opp + aud (5C opportunity). Legacy: craft + fit, then single total.
|
|
@@ -542,6 +669,8 @@ def build_graph() -> dict:
|
|
|
542
669
|
"scout",
|
|
543
670
|
"platform",
|
|
544
671
|
"pillar",
|
|
672
|
+
"lane",
|
|
673
|
+
"generation_mode",
|
|
545
674
|
"one_liner",
|
|
546
675
|
"item",
|
|
547
676
|
"source_type",
|
|
@@ -552,6 +681,9 @@ def build_graph() -> dict:
|
|
|
552
681
|
"src_bookmarks",
|
|
553
682
|
"engagement_at",
|
|
554
683
|
"capture_id",
|
|
684
|
+
"need_ids",
|
|
685
|
+
"product_ids",
|
|
686
|
+
"product_fit",
|
|
555
687
|
"run",
|
|
556
688
|
)
|
|
557
689
|
if topic_rows:
|
|
@@ -655,6 +787,8 @@ def build_graph() -> dict:
|
|
|
655
787
|
"status": row["status"],
|
|
656
788
|
"platform": row["platform"],
|
|
657
789
|
"pillar": row["pillar"],
|
|
790
|
+
"lane": row.get("lane", "") or "",
|
|
791
|
+
"generation_mode": row.get("generation_mode", "") or "",
|
|
658
792
|
"profile": profile,
|
|
659
793
|
"source_type": row["source_type"],
|
|
660
794
|
"src_url": row["src_url"],
|
|
@@ -664,6 +798,9 @@ def build_graph() -> dict:
|
|
|
664
798
|
"src_bookmarks": row["src_bookmarks"],
|
|
665
799
|
"engagement_at": row["engagement_at"],
|
|
666
800
|
"capture_id": row["capture_id"],
|
|
801
|
+
"need_ids": row.get("need_ids", "") or "",
|
|
802
|
+
"product_ids": row.get("product_ids", "") or "",
|
|
803
|
+
"product_fit": row.get("product_fit", "") or "",
|
|
667
804
|
"run_id": row["run"].strip(),
|
|
668
805
|
"usage": usage,
|
|
669
806
|
"src_heat_note": "来源帖热度,不是你的帖",
|
|
@@ -676,8 +813,22 @@ def build_graph() -> dict:
|
|
|
676
813
|
edge(nid("topic", ident), nid("run", row["run"].strip()), "opened_run")
|
|
677
814
|
if item and (ROOT / item).exists():
|
|
678
815
|
body = read(item)
|
|
816
|
+
fm = re.match(r"^---\n(.*?)\n---", body, re.S)
|
|
817
|
+
if fm:
|
|
818
|
+
mneeds = re.search(r"^need_ids:\s*(.*)$", fm.group(1), re.M)
|
|
819
|
+
if mneeds:
|
|
820
|
+
for need_id in re.findall(r"\bN-\d{8}-\d{2}\b", mneeds.group(1)):
|
|
821
|
+
edge(nid("topic", ident), nid("need", need_id), "from_need")
|
|
822
|
+
mproducts = re.search(r"^product_ids:\s*(.*)$", fm.group(1), re.M)
|
|
823
|
+
if mproducts:
|
|
824
|
+
for product_id in PRODUCT_RE.findall(mproducts.group(1)):
|
|
825
|
+
edge(nid("topic", ident), nid("product", product_id), "uses_product")
|
|
826
|
+
mrecommendations = re.search(r"^recommendation_ids:\s*(.*)$", fm.group(1), re.M)
|
|
827
|
+
if mrecommendations:
|
|
828
|
+
for recommendation_id in RECOMMENDATION_RE.findall(mrecommendations.group(1)):
|
|
829
|
+
edge(nid("topic", ident), nid("recommendation", recommendation_id), "reviews")
|
|
679
830
|
chunk = body
|
|
680
|
-
m = re.search(r"^## Suggested production.*?(?=^## |\Z)", body, re.M | re.S)
|
|
831
|
+
m = re.search(r"^## Suggested production.*?(?=^## |\Z)|^## Trace.*?(?=^## |\Z)", body, re.M | re.S)
|
|
681
832
|
if m:
|
|
682
833
|
chunk = m.group(0)
|
|
683
834
|
for line in chunk.splitlines():
|
|
@@ -697,6 +848,12 @@ def build_graph() -> dict:
|
|
|
697
848
|
for cid in ids_in(rest)["claim"]:
|
|
698
849
|
edges.append({"from": nid("topic", ident), "to": nid("claim", cid), "rel": "suggested"})
|
|
699
850
|
|
|
851
|
+
for n in nodes.values():
|
|
852
|
+
if n["kind"] != "need":
|
|
853
|
+
continue
|
|
854
|
+
for tid in re.findall(r"\bT-\d{8}-\d{2}\b", n.get("topic_ids") or ""):
|
|
855
|
+
edge(n["id"], nid("topic", tid), "evidence_for")
|
|
856
|
+
|
|
700
857
|
for row in table_with(
|
|
701
858
|
read("swipe/_index.md"),
|
|
702
859
|
"id",
|
|
@@ -1004,18 +1161,35 @@ def build_graph() -> dict:
|
|
|
1004
1161
|
for cid in field_ids(row["claim"], "claim"):
|
|
1005
1162
|
edge(nid("published", ident), nid("claim", cid), "uses")
|
|
1006
1163
|
|
|
1007
|
-
# Capture →
|
|
1164
|
+
# Capture → source_url from Raw + engagement from entry/raw notes
|
|
1008
1165
|
for n in list(nodes.values()):
|
|
1009
1166
|
if n["kind"] != "capture":
|
|
1010
1167
|
continue
|
|
1168
|
+
raw_node = None
|
|
1011
1169
|
for e in edges:
|
|
1012
1170
|
if e["rel"] != "has_raw" or e["from"] != n["id"]:
|
|
1013
1171
|
continue
|
|
1014
|
-
|
|
1015
|
-
if
|
|
1016
|
-
n["source_url"] =
|
|
1017
|
-
n["raw_id"] =
|
|
1018
|
-
|
|
1172
|
+
raw_node = nodes.get(e["to"])
|
|
1173
|
+
if raw_node and raw_node.get("source_url"):
|
|
1174
|
+
n["source_url"] = raw_node["source_url"]
|
|
1175
|
+
n["raw_id"] = raw_node["ident"]
|
|
1176
|
+
break
|
|
1177
|
+
entry_text = read(n["path"]) if n.get("path") and (ROOT / n["path"]).exists() else ""
|
|
1178
|
+
raw_text = ""
|
|
1179
|
+
raw_path = ""
|
|
1180
|
+
if raw_node and raw_node.get("path"):
|
|
1181
|
+
raw_path = raw_node["path"]
|
|
1182
|
+
elif n.get("raw_path"):
|
|
1183
|
+
raw_path = n["raw_path"] if str(n["raw_path"]).endswith(".md") else f'{n["raw_path"]}.md'
|
|
1184
|
+
if raw_path and (ROOT / raw_path).exists():
|
|
1185
|
+
raw_text = read(raw_path)
|
|
1186
|
+
eng = merge_engagement(
|
|
1187
|
+
parse_engagement_snapshot(entry_text),
|
|
1188
|
+
parse_engagement_snapshot(raw_text),
|
|
1189
|
+
)
|
|
1190
|
+
for k, v in eng.items():
|
|
1191
|
+
if v:
|
|
1192
|
+
n[k] = v
|
|
1019
1193
|
|
|
1020
1194
|
# drop edges to missing nodes
|
|
1021
1195
|
edges = [e for e in edges if e["from"] in nodes and e["to"] in nodes]
|
|
@@ -1056,11 +1230,41 @@ def build_graph() -> dict:
|
|
|
1056
1230
|
"accounts": accounts,
|
|
1057
1231
|
"pillars_by_profile": pillars_by_profile,
|
|
1058
1232
|
"pillars": flat_pillars,
|
|
1233
|
+
"needs_index_present": (ROOT / "needs" / "_index.md").exists(),
|
|
1234
|
+
"products_index_present": (ROOT / "products" / "_index.md").exists(),
|
|
1059
1235
|
"nodes": list(nodes.values()),
|
|
1060
1236
|
"edges": uniq,
|
|
1061
1237
|
}
|
|
1062
1238
|
|
|
1063
1239
|
|
|
1240
|
+
def _self_check() -> None:
|
|
1241
|
+
# ponytail: tiny parser asserts; expand if capture/discovery formats drift again.
|
|
1242
|
+
d = parse_engagement_snapshot(
|
|
1243
|
+
"- **engagement snapshot:** likes 113 / replies 18 / bookmarks 17 / views 9574 @ 2026-08-30\n"
|
|
1244
|
+
)
|
|
1245
|
+
assert d == {
|
|
1246
|
+
"src_likes": "113",
|
|
1247
|
+
"src_replies": "18",
|
|
1248
|
+
"src_bookmarks": "17",
|
|
1249
|
+
"src_views": "9574",
|
|
1250
|
+
"engagement_at": "2026-08-30",
|
|
1251
|
+
}, d
|
|
1252
|
+
c = parse_engagement_snapshot(
|
|
1253
|
+
"6. **Engagement snapshot (fetch 2026-08-25):** ~92.9k views · 1086 likes · 1380 bookmarks · 738 replies · 70 reposts.\n"
|
|
1254
|
+
)
|
|
1255
|
+
assert c["src_views"] == "92900" and c["src_likes"] == "1086", c
|
|
1256
|
+
assert c["src_bookmarks"] == "1380" and c["src_replies"] == "738", c
|
|
1257
|
+
assert c["engagement_at"] == "2026-08-25", c
|
|
1258
|
+
r = parse_engagement_snapshot(
|
|
1259
|
+
"## Engagement snapshot\n\n- likes: 3\n- replies: 3\n- views: 426\n- bookmarks: 2\n- snapshot_at: 2026-09-13\n"
|
|
1260
|
+
)
|
|
1261
|
+
assert r["src_likes"] == "3" and r["src_views"] == "426" and r["engagement_at"] == "2026-09-13", r
|
|
1262
|
+
m = parse_engagement_snapshot(
|
|
1263
|
+
"## Snapshot metrics (at capture)\n\n- Views: ~8091\n- Likes: 103\n- Bookmarks: 29\n- Replies: 20\n"
|
|
1264
|
+
)
|
|
1265
|
+
assert m["src_views"] == "8091" and m["src_likes"] == "103", m
|
|
1266
|
+
assert m["src_bookmarks"] == "29" and m["src_replies"] == "20", m
|
|
1267
|
+
print("parse_engagement_snapshot self-check ok")
|
|
1064
1268
|
def main() -> None:
|
|
1065
1269
|
graph = build_graph()
|
|
1066
1270
|
html = TPL.read_text(encoding="utf-8")
|
|
@@ -1071,4 +1275,9 @@ def main() -> None:
|
|
|
1071
1275
|
|
|
1072
1276
|
|
|
1073
1277
|
if __name__ == "__main__":
|
|
1074
|
-
|
|
1278
|
+
import sys
|
|
1279
|
+
|
|
1280
|
+
if "--self-check" in sys.argv:
|
|
1281
|
+
_self_check()
|
|
1282
|
+
else:
|
|
1283
|
+
main()
|
|
File without changes
|