@nebulacomponents/citable 1.14.0 → 1.15.1

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 (126) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +1 -1
  3. package/dist/universal/.agents/skills/citable/SKILL.md +11 -6
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/commands/README.md +4 -1
  6. package/dist/universal/.agents/skills/citable/commands/observe-and-act.md +12 -5
  7. package/dist/universal/.agents/skills/citable/commands/reporting.md +57 -0
  8. package/dist/universal/.agents/skills/citable/manifest.json +10 -9
  9. package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
  10. package/dist/universal/.agents/skills/citable/schemas/schedule.schema.json +1 -1
  11. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  12. package/dist/universal/.claude/skills/citable/SKILL.md +11 -6
  13. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  14. package/dist/universal/.claude/skills/citable/commands/README.md +4 -1
  15. package/dist/universal/.claude/skills/citable/commands/observe-and-act.md +12 -5
  16. package/dist/universal/.claude/skills/citable/commands/reporting.md +57 -0
  17. package/dist/universal/.claude/skills/citable/manifest.json +10 -9
  18. package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
  19. package/dist/universal/.claude/skills/citable/schemas/schedule.schema.json +1 -1
  20. package/dist/universal/.cursor/skills/citable/SKILL.md +11 -6
  21. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  22. package/dist/universal/.cursor/skills/citable/commands/README.md +4 -1
  23. package/dist/universal/.cursor/skills/citable/commands/observe-and-act.md +12 -5
  24. package/dist/universal/.cursor/skills/citable/commands/reporting.md +57 -0
  25. package/dist/universal/.cursor/skills/citable/manifest.json +10 -9
  26. package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
  27. package/dist/universal/.cursor/skills/citable/schemas/schedule.schema.json +1 -1
  28. package/dist/universal/.gemini/skills/citable/SKILL.md +11 -6
  29. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  30. package/dist/universal/.gemini/skills/citable/commands/README.md +4 -1
  31. package/dist/universal/.gemini/skills/citable/commands/observe-and-act.md +12 -5
  32. package/dist/universal/.gemini/skills/citable/commands/reporting.md +57 -0
  33. package/dist/universal/.gemini/skills/citable/manifest.json +10 -9
  34. package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
  35. package/dist/universal/.gemini/skills/citable/schemas/schedule.schema.json +1 -1
  36. package/dist/universal/.github/skills/citable/SKILL.md +11 -6
  37. package/dist/universal/.github/skills/citable/VERSION +1 -1
  38. package/dist/universal/.github/skills/citable/commands/README.md +4 -1
  39. package/dist/universal/.github/skills/citable/commands/observe-and-act.md +12 -5
  40. package/dist/universal/.github/skills/citable/commands/reporting.md +57 -0
  41. package/dist/universal/.github/skills/citable/manifest.json +10 -9
  42. package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
  43. package/dist/universal/.github/skills/citable/schemas/schedule.schema.json +1 -1
  44. package/dist/universal/.kiro/skills/citable/SKILL.md +11 -6
  45. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  46. package/dist/universal/.kiro/skills/citable/commands/README.md +4 -1
  47. package/dist/universal/.kiro/skills/citable/commands/observe-and-act.md +12 -5
  48. package/dist/universal/.kiro/skills/citable/commands/reporting.md +57 -0
  49. package/dist/universal/.kiro/skills/citable/manifest.json +10 -9
  50. package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
  51. package/dist/universal/.kiro/skills/citable/schemas/schedule.schema.json +1 -1
  52. package/dist/universal/.opencode/skills/citable/SKILL.md +11 -6
  53. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  54. package/dist/universal/.opencode/skills/citable/commands/README.md +4 -1
  55. package/dist/universal/.opencode/skills/citable/commands/observe-and-act.md +12 -5
  56. package/dist/universal/.opencode/skills/citable/commands/reporting.md +57 -0
  57. package/dist/universal/.opencode/skills/citable/manifest.json +10 -9
  58. package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
  59. package/dist/universal/.opencode/skills/citable/schemas/schedule.schema.json +1 -1
  60. package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -6
  61. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  62. package/dist/universal/.pi/agent/skills/citable/commands/README.md +4 -1
  63. package/dist/universal/.pi/agent/skills/citable/commands/observe-and-act.md +12 -5
  64. package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +57 -0
  65. package/dist/universal/.pi/agent/skills/citable/manifest.json +10 -9
  66. package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
  67. package/dist/universal/.pi/agent/skills/citable/schemas/schedule.schema.json +1 -1
  68. package/dist/universal/.qoder/skills/citable/SKILL.md +11 -6
  69. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  70. package/dist/universal/.qoder/skills/citable/commands/README.md +4 -1
  71. package/dist/universal/.qoder/skills/citable/commands/observe-and-act.md +12 -5
  72. package/dist/universal/.qoder/skills/citable/commands/reporting.md +57 -0
  73. package/dist/universal/.qoder/skills/citable/manifest.json +10 -9
  74. package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
  75. package/dist/universal/.qoder/skills/citable/schemas/schedule.schema.json +1 -1
  76. package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -6
  77. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  78. package/dist/universal/.rovodev/skills/citable/commands/README.md +4 -1
  79. package/dist/universal/.rovodev/skills/citable/commands/observe-and-act.md +12 -5
  80. package/dist/universal/.rovodev/skills/citable/commands/reporting.md +57 -0
  81. package/dist/universal/.rovodev/skills/citable/manifest.json +10 -9
  82. package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
  83. package/dist/universal/.rovodev/skills/citable/schemas/schedule.schema.json +1 -1
  84. package/dist/universal/.trae/skills/citable/SKILL.md +11 -6
  85. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  86. package/dist/universal/.trae/skills/citable/commands/README.md +4 -1
  87. package/dist/universal/.trae/skills/citable/commands/observe-and-act.md +12 -5
  88. package/dist/universal/.trae/skills/citable/commands/reporting.md +57 -0
  89. package/dist/universal/.trae/skills/citable/manifest.json +10 -9
  90. package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
  91. package/dist/universal/.trae/skills/citable/schemas/schedule.schema.json +1 -1
  92. package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -6
  93. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  94. package/dist/universal/.trae-cn/skills/citable/commands/README.md +4 -1
  95. package/dist/universal/.trae-cn/skills/citable/commands/observe-and-act.md +12 -5
  96. package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +57 -0
  97. package/dist/universal/.trae-cn/skills/citable/manifest.json +10 -9
  98. package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
  99. package/dist/universal/.trae-cn/skills/citable/schemas/schedule.schema.json +1 -1
  100. package/dist/universal/manifest.json +124 -112
  101. package/package.json +1 -1
  102. package/release/surfaces.json +2 -2
  103. package/schemas/observation.schema.json +1 -1
  104. package/schemas/schedule.schema.json +1 -1
  105. package/skill/SKILL.md +11 -6
  106. package/skill/commands/README.md +4 -1
  107. package/skill/commands/observe-and-act.md +12 -5
  108. package/skill/commands/reporting.md +57 -0
  109. package/src/cli/index.js +78 -11
  110. package/src/commands/connect.js +29 -0
  111. package/src/commands/context.js +15 -0
  112. package/src/commands/delivery.js +86 -2
  113. package/src/commands/monitor.js +35 -0
  114. package/src/commands/observe.js +3 -1
  115. package/src/commands/reportDashboard.js +21 -0
  116. package/src/commands/reportShareOfVoice.js +60 -0
  117. package/src/connectors/http.js +6 -3
  118. package/src/connectors/index.js +3 -1
  119. package/src/connectors/webflow.js +224 -0
  120. package/src/connectors/wordpress.js +226 -0
  121. package/src/detectors/geoReco.js +101 -0
  122. package/src/monitoring/alertDelivery.js +98 -0
  123. package/src/observations/common.js +10 -0
  124. package/src/observations/stance.js +261 -0
  125. package/src/reporting/dashboard.js +254 -0
  126. package/src/reporting/shareOfVoice.js +362 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebulacomponents/citable",
3
- "version": "1.14.0",
3
+ "version": "1.15.1",
4
4
  "description": "Production-grade SEO, AEO, and GEO agent skill with auditable detection, claim governance, evidence validation, and multi-agent installation.",
5
5
  "keywords": [
6
6
  "seo",
@@ -3,7 +3,7 @@
3
3
  "controlled_surfaces": [
4
4
  {
5
5
  "surface_id": "nebula-citable-resource",
6
- "url": "https://nebulacomponents.shop/resources/citable",
6
+ "url": "https://nebulacomponents.com/resources/citable",
7
7
  "projection_id": "resource-data",
8
8
  "verification_method": "response_header",
9
9
  "verification_header": "x-citable-projection-sha256",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  {
13
13
  "surface_id": "nebula-llms-txt",
14
- "url": "https://nebulacomponents.shop/resources/citable/llms.txt",
14
+ "url": "https://nebulacomponents.com/resources/citable/llms.txt",
15
15
  "projection_id": "llms-txt",
16
16
  "verification_method": "exact_response_body",
17
17
  "verification_header": null,
@@ -7,7 +7,7 @@
7
7
  "required": ["observation_id", "kind", "state", "collected_at", "collection_method", "confidence", "source", "evidence_hash", "data"],
8
8
  "properties": {
9
9
  "observation_id": { "type": "string", "minLength": 1 },
10
- "kind": { "type": "string", "enum": ["render", "browser_journey", "index", "citation", "citation_review", "crawler_log", "crawler_probe", "regional_network", "bing_webmaster", "passage", "canonical_freshness", "performance", "corroboration", "metric", "media_pdf", "media_transcript", "media_image", "representation_drift"] },
10
+ "kind": { "type": "string", "enum": ["render", "browser_journey", "index", "citation", "citation_review", "crawler_log", "crawler_probe", "regional_network", "bing_webmaster", "passage", "canonical_freshness", "performance", "corroboration", "metric", "media_pdf", "media_transcript", "media_image", "representation_drift", "stance"] },
11
11
  "state": { "type": "string", "enum": ["observed", "not_observed", "not_evidenced", "incomplete", "review_required", "failed"] },
12
12
  "collected_at": { "type": "string", "format": "date-time" },
13
13
  "collection_method": { "type": "string", "enum": ["live_api", "browser", "owner_import", "synthetic_fetch", "static_analysis", "human_review"] },
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "$id":"citable://schemas/schedule.schema.json","$schema":"http://json-schema.org/draft-07/schema#","title":"Audit schedule registry","type":"object","additionalProperties":false,"required":["version","kind","updated","entries"],
3
- "properties":{"version":{"const":1},"kind":{"const":"schedules"},"updated":{"type":"string","format":"date-time"},"entries":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["schedule_id","name","status","cron","owner","expected_tool_version","audit","retention_days","limitations"],"properties":{"schedule_id":{"type":"string","pattern":"^SCHEDULE-[A-Z0-9][A-Z0-9_-]*$"},"name":{"type":"string","minLength":1},"status":{"type":"string","enum":["active","paused","retired"]},"cron":{"type":"string","minLength":5},"owner":{"type":"string","minLength":1},"expected_tool_version":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+$"},"audit":{"type":"object","additionalProperties":false,"required":["target","scope"],"properties":{"target":{"type":"string","minLength":1},"scope":{"type":["string","null"]},"base_url":{"type":["string","null"]}}},"retention_days":{"type":"integer","minimum":1},"limitations":{"type":"array","minItems":1,"items":{"type":"string"}}}}}}
3
+ "properties":{"version":{"const":1},"kind":{"const":"schedules"},"updated":{"type":"string","format":"date-time"},"entries":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["schedule_id","name","status","cron","owner","expected_tool_version","audit","retention_days","limitations"],"properties":{"schedule_id":{"type":"string","pattern":"^SCHEDULE-[A-Z0-9][A-Z0-9_-]*$"},"name":{"type":"string","minLength":1},"status":{"type":"string","enum":["active","paused","retired"]},"cron":{"type":"string","minLength":5},"owner":{"type":"string","minLength":1},"expected_tool_version":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+$"},"audit":{"type":"object","additionalProperties":false,"required":["target","scope"],"properties":{"target":{"type":"string","minLength":1},"scope":{"type":["string","null"]},"base_url":{"type":["string","null"]}}},"monitor":{"type":"object","additionalProperties":false,"properties":{"enabled":{"type":"boolean"},"webhook_url":{"type":"string"},"min_severity":{"type":"string","enum":["informational","medium","high","critical"]}}},"retention_days":{"type":"integer","minimum":1},"limitations":{"type":"array","minItems":1,"items":{"type":"string"}}}}}}
4
4
  }
package/skill/SKILL.md CHANGED
@@ -7,7 +7,7 @@ description: >
7
7
  generative engine optimization, structured data governance, claim substantiation,
8
8
  crawler policy, entity consistency, content discoverability, or wants a site audited
9
9
  for how search and AI systems will retrieve, understand, cite, or recommend it.
10
- version: 1.14.0
10
+ version: 1.15.1
11
11
  ---
12
12
 
13
13
  # Citable — evidence and change control for search and AI citation readiness
@@ -95,7 +95,8 @@ citable schema --target <dir|url>
95
95
  citable validate [registries|claims|evidence|schema|links]
96
96
  citable compare-snapshots [runA runB]
97
97
  citable action-plan [run-id] # ordered actions, blockers, semantic gates, verification
98
- citable observe <mode> [options] # render/index/citation/log/probe/passage/consensus/performance evidence
98
+ citable observe <mode> [options] # render/index/citation/log/probe/passage/consensus/performance/stance evidence
99
+ citable observe stance --input <citations|prompts.json> [--entity <id>]
99
100
  citable observe bing --dataset <search_performance|ai_performance> --input <csv|json>
100
101
  citable observe probes --target <url> [--region <label>]
101
102
  citable observe network --input <regional-network-import.json>
@@ -103,13 +104,17 @@ citable observe render --target <url> [--interactions] [--resume-run <run-id>]
103
104
  citable observe render --input <browser-evidence-plan.json>
104
105
  citable observe performance --target <url> --lighthouse [--repeat 1..5]
105
106
  citable apply --input <spec> [--write] # reviewed, hash-locked remediation; dry run by default
106
- citable monitor [runA runB] # observation regression alerts
107
+ citable monitor [runA runB] [--webhook <url>] [--min-severity <sev>] # observation regression alerts
108
+ citable report dashboard [--last N] [--since <run-id>] # cross-run evidence trend (Markdown + HTML)
109
+ citable report share-of-voice [--last N] [--since <run-id>] # competitor citation share (Markdown + HTML)
107
110
  citable metrics import --provider <name> --input <csv|json>
108
111
  citable connect status
109
- citable connect configure --provider <gsc|ga4> --connection-id <id> --property-id <id> [--credential-env <name>] [--write]
110
- citable connect discover --provider <gsc|ga4>
112
+ citable connect configure --provider <gsc|ga4|wordpress|webflow> --connection-id <id> --property-id <id> [--credential-env <name>] [--write]
113
+ citable connect discover --provider <gsc|ga4|wordpress|webflow>
111
114
  citable connect validate --connection-id <id>
112
115
  citable connect sync --connection-id <id> --start-date YYYY-MM-DD --end-date YYYY-MM-DD
116
+ citable connect read --connection-id <id> --target-id <id>
117
+ citable connect apply --connection-id <id> --input <cms-spec.json> [--write]
113
118
  citable connect disconnect --connection-id <id> [--write]
114
119
  citable objectives init --input <json|yaml> [--write]
115
120
  citable objectives validate
@@ -121,7 +126,7 @@ citable reviews prioritize [--write]
121
126
  citable reviews plan --input <json|yaml> [--write]
122
127
  citable reviews sample <sampling-plan-id> [--write]
123
128
  citable reviews evaluate
124
- citable schedules run <schedule-id> [--ref-date YYYY-MM-DD]
129
+ citable schedules run <schedule-id> [--ref-date YYYY-MM-DD] [--monitor] [--webhook <url>]
125
130
  citable project github <run-id>
126
131
  citable observe media --input <manifest.json> [--ocr]
127
132
  citable observe representation --input <release-manifest.json> --target <controlled-url>
@@ -21,9 +21,12 @@ intent, not executability, so statuses are strict:
21
21
  | /citable validate, validate-claims, validate-evidence, validate-schema, validate-links | implemented |
22
22
  | /citable compare-snapshots / validate-regression | implemented |
23
23
  | /citable action-plan | implemented — writes ordered, source-run-bound action artifacts outside immutable evidence packages |
24
- | /citable observe | implemented — render, index, citation, crawler-log, passage, consensus, performance, and corroboration evidence |
24
+ | /citable observe | implemented — render, index, citation, crawler-log, passage, consensus, performance, corroboration, and stance evidence |
25
25
  | /citable apply | implemented — reviewed, hash-locked source replacements; dry run by default |
26
+ | /citable connect status / configure / discover / validate / sync / read / apply / disconnect | implemented — GA4, GSC, WordPress, and Webflow adapters with hash-locked CMS remediation |
26
27
  | /citable monitor | implemented — compares immutable observation runs and emits evidence-linked alerts |
28
+ | /citable report dashboard | implemented — folds each run's recorded summary into a cross-run Markdown/HTML trend; derives no new findings and no combined score |
29
+ | /citable report share-of-voice | implemented — joins competitor registry against recorded citation observations to compute verified share-of-citation |
27
30
  | /citable metrics import | implemented — validates declared CSV/JSON metrics and writes immutable observations |
28
31
  | /citable objectives init / validate | implemented — user-owned metric selection, cohorts, windows, and guardrails |
29
32
  | /citable evaluate | implemented — independent baseline/evaluation comparisons with inconclusive handling |
@@ -91,9 +91,16 @@ reviewer, matches the exact file hash, and identifies exactly one source string.
91
91
  `--write` applies only after all operations validate. Build, tests, semantic
92
92
  review, re-audit, and snapshot comparison remain mandatory afterward.
93
93
 
94
- ## Monitoring
94
+ ## Monitoring and alert delivery
95
95
 
96
- `monitor [runA runB]` reports state changes, missing observations, index loss,
97
- canonical disagreement, and citation-presence changes. Alerts are evidence
98
- changes, not causal explanations or ranking conclusions. Schedule it in CI at
99
- the cadence defined by lifecycle and experiment records.
96
+ `monitor [runA runB] [--webhook <url>] [--min-severity <level>]` reports state
97
+ changes, missing observations, index loss, canonical disagreement, and
98
+ citation-presence changes. With `--webhook`, detected regressions matching or
99
+ exceeding `--min-severity` (default `medium`) are dispatched as sealed JSON
100
+ alert payloads and logged under `.citable/monitoring/deliveries/`. Destination
101
+ URLs must be public network destinations (private/loopback addresses are blocked).
102
+ Alerts are evidence changes, not causal explanations or ranking conclusions.
103
+
104
+ Scheduled monitoring (`schedules run <id> --monitor [--webhook <url>]`) automatically
105
+ compares the scheduled audit against the preceding baseline run and dispatches
106
+ regression alerts when configured in `schedules.yaml`.
@@ -0,0 +1,57 @@
1
+ ---
2
+ command: /citable report dashboard [--last N] [--since <run-id>]
3
+ purpose: Fold the summary each audit run already recorded into a cross-run Markdown table and self-contained HTML page without deriving new findings, scores, or causal explanations.
4
+ preconditions: [at least one finalized audit run under .citable/runs/ with a summary.json]
5
+ failure_behaviour: fewer than two audit runs -> explicit insufficient-history state, not a trend; unreadable summary or manifest -> run reported under skipped runs; non-integer --last -> fail
6
+ artifacts_created: [.citable/reports/dashboard.md, .citable/reports/dashboard.html]
7
+ ---
8
+
9
+ # Cross-run evidence reporting
10
+
11
+ 1. Run `citable report dashboard` to read severity counts, retrieval
12
+ eligibility, source extraction and support, and observed citation presence
13
+ rate across run history. Every value is copied from a run's own
14
+ `summary.json`. The command runs no detector, opens no network connection,
15
+ and never edits an immutable run package.
16
+ 2. Treat the output as derived, not canonical. It is written to
17
+ `.citable/reports/`, alongside `monitor` and `action-plan` derived
18
+ artifacts. The evidence of record remains `.citable/runs/<run-id>/`.
19
+ 3. Window the history with `--since <run-id>` (keeps that run and everything
20
+ after it) and `--last N` (keeps the final N runs). Run IDs sort
21
+ chronologically, so both filters are deterministic.
22
+ 4. Read fewer than two runs as no trend. With zero or one run the dashboard
23
+ states insufficient history explicitly instead of drawing a line through a
24
+ single point. Direction, rate of change, and improvement are not established
25
+ by one observation.
26
+ 5. Read skipped runs as a gap in the series, not as absence of a problem. A run
27
+ whose `summary.json` or `manifest.json` cannot be parsed, and an observation
28
+ run whose summary records no severity or posture, is listed with its reason
29
+ rather than counted as zero findings.
30
+ 6. Read a falling count as an observed difference between runs, not as proof
31
+ that a condition was fixed. Use `citable compare-snapshots` to establish
32
+ whether two runs are comparable at all — detector set, configuration,
33
+ observation method, and tool version all change what a count means.
34
+
35
+ # Refusal boundary
36
+
37
+ Do not present the dashboard as an AI visibility score, a ranking forecast, or
38
+ a measure of citation likelihood. The three posture dimensions are reported
39
+ separately and are never combined into a single number. Do not interpolate,
40
+ estimate, or carry forward a missing value: a run with no evidenced citation
41
+ presence rate is omitted from that trend line, not filled in. Do not attribute
42
+ any observed change to a specific intervention.
43
+
44
+ # Share of voice and competitor reporting
45
+
46
+ 1. Run `citable report share-of-voice [--last N] [--since <run-id>]` to evaluate
47
+ first-party and competitor citation presence and share across recorded
48
+ `observe citation` runs.
49
+ 2. Contested competitor domains are loaded from `.citable/competitors.yaml`.
50
+ Citations whose canonical URL matches declared competitor domains are joined
51
+ into per-prompt and aggregate shares.
52
+ 3. Competitor share is calculated strictly from evidenced observations; missing
53
+ observations or unevaluated prompts are never filled with fabricated zeros or
54
+ interpolated estimates.
55
+ 4. Outputs are written to `.citable/reports/share-of-voice.md` and
56
+ `.citable/reports/share-of-voice.html`.
57
+
package/src/cli/index.js CHANGED
@@ -16,9 +16,11 @@ import { projectGithub, runSchedule } from '../commands/delivery.js';
16
16
  import { actionPlan } from '../commands/actionPlan.js';
17
17
  import { observe } from '../commands/observe.js';
18
18
  import { applyRemediation } from '../commands/applyRemediation.js';
19
- import { monitor } from '../commands/monitor.js';
19
+ import { monitor, monitorAndAlert } from '../commands/monitor.js';
20
+ import { reportDashboard } from '../commands/reportDashboard.js';
21
+ import { reportShareOfVoice } from '../commands/reportShareOfVoice.js';
20
22
  import { evaluateObjective, importMetrics, initializeObjective, validateObjectives } from '../commands/measurement.js';
21
- import { configureConnection, connectionStatus, discoverConnections, disconnectConnection, syncConnection, validateConnection } from '../commands/connect.js';
23
+ import { configureConnection, connectionStatus, discoverConnections, disconnectConnection, syncConnection, validateConnection, readCmsContent, applyCmsRemediation } from '../commands/connect.js';
22
24
  import { evaluateDispositions, validateGovernance } from '../commands/governance.js';
23
25
  import { evaluateReviews, initializeSamplingPlan, prioritizeReviews, queueReviews, selectSample } from '../commands/reviews.js';
24
26
  import { selfUpgradeCommand, selfUpgradeExitCode } from '../commands/selfUpgrade.js';
@@ -67,13 +69,17 @@ Commands
67
69
  consensus, performance, corroboration, crawler or regional probes,
68
70
  media evidence, or representation evidence
69
71
  apply Apply a reviewed, hash-locked remediation spec
70
- monitor [runA runB] Compare observation runs and emit regression alerts
72
+ monitor [runA runB] Compare observation runs and emit regression alerts [--webhook <url>]
73
+ report dashboard [--last N] [--since <run-id>] Render a cross-run evidence trend as Markdown + HTML
74
+ report share-of-voice [--last N] Compute first-party and competitor citation share
71
75
  metrics import Import declared metric observations from CSV/JSON
72
76
  connect status List optional connectors and configured connections
73
77
  connect configure Configure non-secret connection state (--write to save)
74
78
  connect discover Discover provider properties using environment auth
75
79
  connect validate Verify configured property access
76
80
  connect sync Collect declared metrics into immutable observations
81
+ connect read Read CMS content and hash for remediation targeting
82
+ connect apply Apply reviewed, hash-locked CMS remediation (--write to apply)
77
83
  connect disconnect Remove optional connection state (--write to confirm)
78
84
  objectives init Validate/add one objective from --input (--write to save)
79
85
  objectives validate Validate objective contracts and metric references
@@ -85,7 +91,7 @@ Commands
85
91
  reviews plan Validate/add a sampling plan from --input
86
92
  reviews sample [plan] Select a reproducible census or seeded random sample
87
93
  reviews evaluate Detect stale decisions and require disagreement adjudication
88
- schedules run [id] Execute an active version-pinned audit schedule
94
+ schedules run [id] Execute an active version-pinned schedule [--monitor] [--webhook <url>]
89
95
  project github [run] Render non-authoritative GitHub annotations from a run
90
96
  corpus evaluate Evaluate a disclosed real-property acceptance corpus
91
97
  corpus publish Validate and project an owner-authorized public corpus
@@ -156,6 +162,8 @@ function parseArgs(argv) {
156
162
  else if (a === '--input') args.input = argv[++i];
157
163
  else if (a === '--output') args.output = argv[++i];
158
164
  else if (a === '--run') args.runId = argv[++i];
165
+ else if (a === '--since') args.since = argv[++i];
166
+ else if (a === '--last') args.last = argv[++i];
159
167
  else if (a === '--provider') args.provider = argv[++i];
160
168
  else if (a === '--dataset') args.dataset = argv[++i];
161
169
  else if (a === '--connection-id') args.connectionId = argv[++i];
@@ -175,6 +183,12 @@ function parseArgs(argv) {
175
183
  else if (a === '--timeout') args.timeout = Number(argv[++i]);
176
184
  else if (a === '--force') args.force = true;
177
185
  else if (a === '--seed') args.seed = argv[++i];
186
+ else if (a === '--webhook') args.webhook = argv[++i];
187
+ else if (a === '--min-severity') args.minSeverity = argv[++i];
188
+ else if (a === '--monitor') args.monitor = true;
189
+ else if (a === '--target-id') args.targetId = argv[++i];
190
+ else if (a === '--reviewer') args.reviewer = argv[++i];
191
+ else if (a === '--entity') args.entity = argv[++i];
178
192
  else args._.push(a);
179
193
  }
180
194
  return args;
@@ -276,7 +290,9 @@ export async function main(argv = process.argv.slice(2), options = {}) {
276
290
  const r = await observe(root, mode, args);
277
291
  const claimDiffLine = r.summary.citation_metrics?.claim_diff
278
292
  ? `\nClaim diff: ${Object.entries(r.summary.citation_metrics.claim_diff).map(([k, v]) => `${k}:${v}`).join(' ')}` : '';
279
- out(args, `observe ${mode}: ${r.summary.total} observation(s) [${Object.entries(r.summary.by_state).map(([k, v]) => `${k}:${v}`).join(' ')}]${claimDiffLine}\nEvidence package: ${r.dir}\nStatus: ${r.manifest.status}`, r);
293
+ const stanceLine = r.summary.stance_metrics
294
+ ? `\nStance summary: favorable:${r.summary.stance_metrics.favorable} neutral:${r.summary.stance_metrics.neutral} unfavorable:${r.summary.stance_metrics.unfavorable} mixed:${r.summary.stance_metrics.mixed} review_required:${r.summary.stance_metrics.review_required}` : '';
295
+ out(args, `observe ${mode}: ${r.summary.total} observation(s) [${Object.entries(r.summary.by_state).map(([k, v]) => `${k}:${v}`).join(' ')}]${claimDiffLine}${stanceLine}\nEvidence package: ${r.dir}\nStatus: ${r.manifest.status}`, r);
280
296
  break;
281
297
  }
282
298
  case 'apply': {
@@ -285,11 +301,35 @@ export async function main(argv = process.argv.slice(2), options = {}) {
285
301
  break;
286
302
  }
287
303
  case 'monitor': {
288
- const r = monitor(root, { runA: args._[0], runB: args._[1] });
289
- out(args, `monitor ${r.run_a} → ${r.run_b}: ${r.summary.alerts} alert(s), ${r.summary.critical_or_high} critical/high\nReport: ${path.join(r.dir, 'latest.json')}`, r);
304
+ const r = await monitorAndAlert(root, {
305
+ runA: args._[0],
306
+ runB: args._[1],
307
+ webhookUrl: args.webhook,
308
+ minSeverity: args.minSeverity,
309
+ });
310
+ let msg = `monitor ${r.run_a} → ${r.run_b}: ${r.summary.alerts} alert(s), ${r.summary.critical_or_high} critical/high\nReport: ${path.join(r.dir, 'latest.json')}`;
311
+ if (r.delivery) {
312
+ if (r.delivery.skipped) msg += `\nDelivery: skipped (${r.delivery.reason})`;
313
+ else if (r.delivery.success) msg += `\nDelivery: webhook dispatched to ${r.delivery.target_url} (HTTP ${r.delivery.status_code})`;
314
+ else msg += `\nDelivery: webhook FAILED (${r.delivery.error})`;
315
+ }
316
+ out(args, msg, r);
290
317
  if (r.summary.critical_or_high > 0) process.exitCode = 1;
291
318
  break;
292
319
  }
320
+ case 'report': {
321
+ const sub = args._[0];
322
+ if (sub === 'dashboard') {
323
+ const r = reportDashboard(root, { since: args.since, last: args.last ? Number(args.last) : undefined });
324
+ out(args, `report dashboard: ${r.included} audit run(s) included, ${r.skipped} skipped\nMarkdown: ${r.path_md}\nHTML: ${r.path_html}`, r);
325
+ } else if (sub === 'share-of-voice' || sub === 'citations' || sub === 'share') {
326
+ const r = reportShareOfVoice(root, { since: args.since, last: args.last ? Number(args.last) : undefined });
327
+ out(args, `report share-of-voice: ${r.included} citation run(s) included, ${r.competitors_evaluated} competitor(s) evaluated\nMarkdown: ${r.path_md}\nHTML: ${r.path_html}`, r);
328
+ } else {
329
+ throw new Error('usage: citable report <dashboard|share-of-voice> [--last <n>] [--since <run-id>]');
330
+ }
331
+ break;
332
+ }
293
333
  case 'metrics': {
294
334
  if (args._[0] !== 'import') throw new Error('usage: citable metrics import --provider <name> --input <csv|json>');
295
335
  const r = importMetrics(root, { input: args.input, provider: args.provider });
@@ -314,10 +354,16 @@ export async function main(argv = process.argv.slice(2), options = {}) {
314
354
  } else if (mode === 'sync') {
315
355
  const r = await syncConnection(root, args);
316
356
  out(args, `connect sync ${r.connection_id}: ${r.summary.total} metric observation(s)\nEvidence package: ${r.dir}`, r);
357
+ } else if (mode === 'read') {
358
+ const r = await readCmsContent(root, args);
359
+ out(args, `connect read ${r.connection_id} [${r.target_id}]: title="${r.content.title}" hash=${r.content.content_hash.slice(0, 12)}…\nURL: ${r.content.url || 'n/a'}`, r);
360
+ } else if (mode === 'apply') {
361
+ const r = await applyCmsRemediation(root, args);
362
+ out(args, `connect apply ${r.connection_id} [${r.target_id}]: ${r.status} ${r.dry_run ? '(dry run; use --write to apply)' : 'applied'}\nBefore: ${r.before_hash?.slice(0, 12)}… After: ${r.after_hash?.slice(0, 12)}…\nReviewer: ${r.reviewer}`, r);
317
363
  } else if (mode === 'disconnect') {
318
364
  const r = disconnectConnection(root, args);
319
365
  out(args, `connect disconnect ${r.connection_id}: ${r.disconnected ? 'removed' : 'dry run; use --write to remove'}`, r);
320
- } else throw new Error('usage: citable connect <status|configure|discover|validate|sync> [options]');
366
+ } else throw new Error('usage: citable connect <status|configure|discover|validate|sync|read|apply|disconnect> [options]');
321
367
  break;
322
368
  }
323
369
  case 'objectives': {
@@ -363,9 +409,30 @@ export async function main(argv = process.argv.slice(2), options = {}) {
363
409
  break;
364
410
  }
365
411
  case 'schedules': {
366
- if(args._[0]!=='run') throw new Error('usage: citable schedules run <schedule-id> [--ref-date YYYY-MM-DD]');
367
- const r=await runSchedule(root,{scheduleId:args._[1],refDate:args.refDate});
368
- out(args,`schedule ${r.schedule_execution.schedule_id}: audit ${r.runId}\nEvidence package: ${r.dir}\nExecution record: ${r.execution_file}`,r);
412
+ if (args._[0] !== 'run') throw new Error('usage: citable schedules run <schedule-id> [--ref-date YYYY-MM-DD] [--webhook <url>] [--monitor]');
413
+ const r = await runSchedule(root, {
414
+ scheduleId: args._[1],
415
+ refDate: args.refDate,
416
+ webhook: args.webhook,
417
+ monitor: args.monitor,
418
+ minSeverity: args.minSeverity,
419
+ });
420
+ let msg = `schedule ${r.schedule_execution.schedule_id}: audit ${r.runId}\nEvidence package: ${r.dir}\nExecution record: ${r.execution_file}`;
421
+ if (r.schedule_execution.monitor) {
422
+ const m = r.schedule_execution.monitor;
423
+ if (m.status === 'insufficient_history') {
424
+ msg += `\nMonitor: insufficient history (${m.message})`;
425
+ } else {
426
+ msg += `\nMonitor: compared to ${m.baseline_run} (${m.regressions_count} regression(s), ${m.summary.regression_critical_or_high} critical/high)`;
427
+ }
428
+ }
429
+ if (r.schedule_execution.alert_delivery) {
430
+ const d = r.schedule_execution.alert_delivery;
431
+ if (d.skipped) msg += `\nAlert delivery: skipped (${d.reason})`;
432
+ else if (d.success) msg += `\nAlert delivery: dispatched to ${d.target_url} (HTTP ${d.status_code})`;
433
+ else msg += `\nAlert delivery: FAILED (${d.error})`;
434
+ }
435
+ out(args, msg, r);
369
436
  break;
370
437
  }
371
438
  case 'project': {
@@ -1,7 +1,9 @@
1
+ import fs from 'node:fs';
1
2
  import { loadRegistries, saveRegistry } from '../registries/index.js';
2
3
  import { envelope, observationRun } from '../observations/common.js';
3
4
  import { getConnector, listConnectors } from '../connectors/index.js';
4
5
  import { validateAgainst } from '../shared/schemaValidator.js';
6
+ import { readJson } from '../shared/io.js';
5
7
 
6
8
  function findConnection(root, connectionId) {
7
9
  const loaded = loadRegistries(root);
@@ -88,3 +90,30 @@ export async function syncConnection(root, { connectionId, startDate, endDate, a
88
90
  saveRegistry(root, 'connections', updated);
89
91
  return { ...run, connection_id: connectionId, provider: connector.provider };
90
92
  }
93
+
94
+ export async function readCmsContent(root, { connectionId, targetId, accessToken, env, fetchImpl } = {}) {
95
+ if (!connectionId || !targetId) throw new Error('connect read requires --connection-id <id> and --target-id <id>');
96
+ const loaded = findConnection(root, connectionId);
97
+ const connector = getConnector(loaded.connection.provider);
98
+ if (!connector.readContent) {
99
+ throw new Error(`connector ${connector.provider} does not support CMS content reading`);
100
+ }
101
+ const context = contextFor(loaded.connection, connector, { accessToken, env, fetchImpl });
102
+ const content = await connector.readContent(loaded.connection, targetId, context);
103
+ return { connection_id: connectionId, provider: connector.provider, target_id: targetId, content };
104
+ }
105
+
106
+ export async function applyCmsRemediation(root, { connectionId, input, write = false, reviewer, accessToken, env, fetchImpl } = {}) {
107
+ if (!connectionId || !input) throw new Error('connect apply requires --connection-id <id> and --input <spec.json>');
108
+ const loaded = findConnection(root, connectionId);
109
+ const connector = getConnector(loaded.connection.provider);
110
+ if (!connector.applyRemediation) {
111
+ throw new Error(`connector ${connector.provider} does not support CMS remediation updates`);
112
+ }
113
+ const spec = typeof input === 'string' && fs.existsSync(input) ? readJson(input) : typeof input === 'object' ? input : JSON.parse(input);
114
+ const context = { ...contextFor(loaded.connection, connector, { accessToken, env, fetchImpl }), write: Boolean(write) };
115
+ if (reviewer && !spec.reviewer) spec.reviewer = reviewer;
116
+ const result = await connector.applyRemediation(loaded.connection, spec, context);
117
+ return { connection_id: connectionId, provider: connector.provider, ...result };
118
+ }
119
+
@@ -41,6 +41,20 @@ export async function buildContext(root, { target, baseUrl, refDate } = {}) {
41
41
  }
42
42
  }
43
43
 
44
+ // Observations recorded under .citable/runs/*/observations/
45
+ const observations = [];
46
+ if (fs.existsSync(runsDir)) {
47
+ for (const run of fs.readdirSync(runsDir)) {
48
+ const obsDir = path.join(runsDir, run, 'observations');
49
+ if (!fs.existsSync(obsDir)) continue;
50
+ for (const f of fs.readdirSync(obsDir)) {
51
+ if (f.endsWith('.json')) {
52
+ try { observations.push(readJson(path.join(obsDir, f))); } catch { warnings.push(`unreadable observation: ${run}/${f}`); }
53
+ }
54
+ }
55
+ }
56
+ }
57
+
44
58
  // Latest page snapshot (for regression/freshness comparison)
45
59
  let snapshots = null;
46
60
  const snapFile = path.join(root, '.citable', 'snapshots', 'pages-latest.json');
@@ -54,6 +68,7 @@ export async function buildContext(root, { target, baseUrl, refDate } = {}) {
54
68
  registries,
55
69
  site,
56
70
  promptResults: promptResults.length ? promptResults : null,
71
+ observations: observations.length ? observations : null,
57
72
  snapshots,
58
73
  refDate: parseRefDate(refDate),
59
74
  hashPage: (p) => sha256(p.text),
@@ -2,12 +2,23 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { audit } from './audit.js';
5
+ import { compareSnapshots } from './compareSnapshots.js';
6
+ import { buildAlertPayload, dispatchAlertWebhook, filterAlerts } from '../monitoring/alertDelivery.js';
5
7
  import { loadRegistries } from '../registries/index.js';
6
8
  import { readJson, sha256, writeJson, nowIso } from '../shared/io.js';
7
9
 
8
10
  const PKG = readJson(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../package.json'));
9
11
 
10
- export async function runSchedule(root, { scheduleId, refDate } = {}) {
12
+ export async function runSchedule(root, {
13
+ scheduleId,
14
+ refDate,
15
+ webhook,
16
+ monitor: flagMonitor,
17
+ minSeverity,
18
+ fetchImpl,
19
+ lookup,
20
+ allowPrivateForTest = false,
21
+ } = {}) {
11
22
  const { registries, problems } = loadRegistries(root);
12
23
  if (problems.length) throw new Error(`registry validation failed: ${problems.join('; ')}`);
13
24
  const schedule = registries.schedules.entries.find((item) => item.schedule_id === scheduleId);
@@ -15,7 +26,80 @@ export async function runSchedule(root, { scheduleId, refDate } = {}) {
15
26
  if (schedule.status !== 'active') throw new Error(`schedule ${scheduleId} is ${schedule.status}`);
16
27
  if (schedule.expected_tool_version !== PKG.version) throw new Error(`schedule ${scheduleId} expects Citable ${schedule.expected_tool_version}, current version is ${PKG.version}`);
17
28
  const result = await audit(root, { target: schedule.audit.target, scope: schedule.audit.scope, baseUrl: schedule.audit.base_url, refDate });
18
- const execution = { schedule_id: scheduleId, cron: schedule.cron, owner: schedule.owner, executed_at: nowIso(), run_id: result.runId, manifest_hash: sha256(fs.readFileSync(path.join(result.dir, 'manifest.json'))), limitations: schedule.limitations };
29
+
30
+ const shouldMonitor = flagMonitor || Boolean(schedule.monitor?.enabled);
31
+ let monitorResult = null;
32
+ let alertDelivery = null;
33
+
34
+ if (shouldMonitor) {
35
+ const runsDir = path.join(root, '.citable', 'runs');
36
+ const runs = fs.existsSync(runsDir)
37
+ ? fs.readdirSync(runsDir).filter((r) => r !== result.runId && fs.existsSync(path.join(runsDir, r, 'findings.json'))).sort()
38
+ : [];
39
+ const prevRunId = runs.at(-1);
40
+
41
+ if (prevRunId) {
42
+ const comparison = compareSnapshots(root, { runA: prevRunId, runB: result.runId });
43
+ monitorResult = {
44
+ baseline_run: prevRunId,
45
+ comparable: comparison.comparability.comparable,
46
+ summary: comparison.summary,
47
+ regressions_count: comparison.regressions.length,
48
+ };
49
+
50
+ const targetWebhook = webhook || schedule.monitor?.webhook_url;
51
+ const targetMinSev = minSeverity || schedule.monitor?.min_severity || 'medium';
52
+
53
+ if (targetWebhook) {
54
+ const alerts = comparison.regressions.map((f) => ({
55
+ severity: f.classification?.severity || 'medium',
56
+ type: 'finding_regression',
57
+ detector_id: f.detector_id,
58
+ key: `${f.detector_id}|${f.subject?.identifier || f.subject?.source_file || 'unknown'}`,
59
+ message: f.observation?.summary || '',
60
+ }));
61
+
62
+ const qualifyingAlerts = filterAlerts(alerts, targetMinSev);
63
+ if (qualifyingAlerts.length > 0) {
64
+ const payload = buildAlertPayload({
65
+ runA: prevRunId,
66
+ runB: result.runId,
67
+ source: 'schedule',
68
+ summary: {
69
+ schedule_id: scheduleId,
70
+ ...comparison.summary,
71
+ },
72
+ alerts: qualifyingAlerts,
73
+ });
74
+ alertDelivery = await dispatchAlertWebhook(root, payload, {
75
+ webhookUrl: targetWebhook,
76
+ fetchImpl,
77
+ lookup,
78
+ allowPrivateForTest,
79
+ });
80
+ } else {
81
+ alertDelivery = { skipped: true, reason: `no regressions meet min_severity: ${targetMinSev}` };
82
+ }
83
+ }
84
+ } else {
85
+ monitorResult = {
86
+ status: 'insufficient_history',
87
+ message: 'No prior audit run available for regression comparison',
88
+ };
89
+ }
90
+ }
91
+
92
+ const execution = {
93
+ schedule_id: scheduleId,
94
+ cron: schedule.cron,
95
+ owner: schedule.owner,
96
+ executed_at: nowIso(),
97
+ run_id: result.runId,
98
+ manifest_hash: sha256(fs.readFileSync(path.join(result.dir, 'manifest.json'))),
99
+ limitations: schedule.limitations,
100
+ ...(monitorResult ? { monitor: monitorResult } : {}),
101
+ ...(alertDelivery ? { alert_delivery: alertDelivery } : {}),
102
+ };
19
103
  const executionFile = path.join(root, '.citable', 'schedule-executions', `${result.runId}.json`);
20
104
  writeJson(executionFile, execution);
21
105
  return { ...result, schedule_execution: execution, execution_file: executionFile };
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { readJson, writeJson, nowIso } from '../shared/io.js';
4
+ import { buildAlertPayload, dispatchAlertWebhook, filterAlerts } from '../monitoring/alertDelivery.js';
4
5
 
5
6
  function observations(dir) {
6
7
  const folder = path.join(dir, 'observations');
@@ -47,3 +48,37 @@ export function monitor(root, { runA, runB } = {}) {
47
48
  writeJson(path.join(dir, 'latest.json'), result);
48
49
  return { ...result, dir };
49
50
  }
51
+
52
+ export async function monitorAndAlert(root, {
53
+ runA,
54
+ runB,
55
+ webhookUrl,
56
+ minSeverity = 'medium',
57
+ fetchImpl,
58
+ lookup,
59
+ allowPrivateForTest = false,
60
+ } = {}) {
61
+ const result = monitor(root, { runA, runB });
62
+ if (webhookUrl) {
63
+ const qualifying = filterAlerts(result.alerts, minSeverity);
64
+ if (qualifying.length > 0) {
65
+ const payload = buildAlertPayload({
66
+ runA: result.run_a,
67
+ runB: result.run_b,
68
+ source: 'monitor',
69
+ summary: result.summary,
70
+ alerts: qualifying,
71
+ });
72
+ const delivery = await dispatchAlertWebhook(root, payload, {
73
+ webhookUrl,
74
+ fetchImpl,
75
+ lookup,
76
+ allowPrivateForTest,
77
+ });
78
+ return { ...result, delivery };
79
+ }
80
+ return { ...result, delivery: { skipped: true, reason: `no alerts meet min_severity: ${minSeverity}` } };
81
+ }
82
+ return result;
83
+ }
84
+
@@ -12,6 +12,7 @@ import { observeMedia } from '../observations/media.js';
12
12
  import { parse as parseHtml } from 'node-html-parser';
13
13
  import { verifyManifestIntegrity } from '../release/governance.js';
14
14
  import { observeBrowserPlan } from '../observations/browserJourney.js';
15
+ import { observeStance } from '../observations/stance.js';
15
16
 
16
17
  const originOf = (value) => { try { return new URL(value).origin; } catch { return null; } };
17
18
  const words = (text) => String(text || '').trim().split(/\s+/).filter(Boolean);
@@ -761,6 +762,7 @@ export async function observe(root, mode, options = {}) {
761
762
  case 'network': return observeRegionalNetwork(root, options);
762
763
  case 'media': return observeMedia(root, options);
763
764
  case 'representation': return observeRepresentation(root, options);
764
- default: throw new Error('observe mode must be render, index, citations, logs, bing, passages, consensus, performance, corroboration, probes, network, media, or representation');
765
+ case 'stance': return observeStance(root, options);
766
+ default: throw new Error('observe mode must be render, index, citations, logs, bing, passages, consensus, performance, corroboration, probes, network, media, representation, or stance');
765
767
  }
766
768
  }
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { buildTimeSeries, loadRunHistory, renderDashboardHtml, renderDashboardMarkdown } from '../reporting/dashboard.js';
4
+
5
+ /**
6
+ * `citable report dashboard` — fold the summary already recorded by each audit
7
+ * run into a cross-run Markdown table and a self-contained HTML page. Derived
8
+ * output only: it never reads, edits, or re-derives an immutable run package.
9
+ */
10
+ export function reportDashboard(root, { since, last } = {}) {
11
+ if (last != null && (!Number.isInteger(last) || last < 1)) throw new Error('--last must be a positive integer');
12
+ const { included, skipped } = loadRunHistory(root, { since, last });
13
+ const series = buildTimeSeries(included);
14
+ const dir = path.join(root, '.citable', 'reports');
15
+ const pathMd = path.join(dir, 'dashboard.md');
16
+ const pathHtml = path.join(dir, 'dashboard.html');
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ fs.writeFileSync(pathMd, renderDashboardMarkdown(series, skipped));
19
+ fs.writeFileSync(pathHtml, renderDashboardHtml(series, skipped));
20
+ return { dir, included: included.length, skipped: skipped.length, path_md: pathMd, path_html: pathHtml };
21
+ }