@nebulacomponents/citable 1.14.0 → 1.15.0
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/CHANGELOG.md +66 -0
- package/README.md +1 -1
- package/dist/universal/.agents/skills/citable/SKILL.md +11 -6
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/commands/README.md +4 -1
- package/dist/universal/.agents/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.agents/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.agents/skills/citable/manifest.json +10 -9
- package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.agents/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +11 -6
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/commands/README.md +4 -1
- package/dist/universal/.claude/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.claude/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.claude/skills/citable/manifest.json +10 -9
- package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/SKILL.md +11 -6
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/commands/README.md +4 -1
- package/dist/universal/.cursor/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.cursor/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +10 -9
- package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/SKILL.md +11 -6
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/commands/README.md +4 -1
- package/dist/universal/.gemini/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.gemini/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +10 -9
- package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.github/skills/citable/SKILL.md +11 -6
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/commands/README.md +4 -1
- package/dist/universal/.github/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.github/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.github/skills/citable/manifest.json +10 -9
- package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/SKILL.md +11 -6
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/commands/README.md +4 -1
- package/dist/universal/.kiro/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.kiro/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +10 -9
- package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/SKILL.md +11 -6
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/commands/README.md +4 -1
- package/dist/universal/.opencode/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.opencode/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +10 -9
- package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -6
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +4 -1
- package/dist/universal/.pi/agent/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +10 -9
- package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/SKILL.md +11 -6
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/commands/README.md +4 -1
- package/dist/universal/.qoder/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.qoder/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +10 -9
- package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -6
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/README.md +4 -1
- package/dist/universal/.rovodev/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.rovodev/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +10 -9
- package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/SKILL.md +11 -6
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/commands/README.md +4 -1
- package/dist/universal/.trae/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.trae/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.trae/skills/citable/manifest.json +10 -9
- package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -6
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +4 -1
- package/dist/universal/.trae-cn/skills/citable/commands/observe-and-act.md +12 -5
- package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +57 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +10 -9
- package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/schedule.schema.json +1 -1
- package/dist/universal/manifest.json +124 -112
- package/package.json +1 -1
- package/schemas/observation.schema.json +1 -1
- package/schemas/schedule.schema.json +1 -1
- package/skill/SKILL.md +11 -6
- package/skill/commands/README.md +4 -1
- package/skill/commands/observe-and-act.md +12 -5
- package/skill/commands/reporting.md +57 -0
- package/src/cli/index.js +78 -11
- package/src/commands/connect.js +29 -0
- package/src/commands/context.js +15 -0
- package/src/commands/delivery.js +86 -2
- package/src/commands/monitor.js +35 -0
- package/src/commands/observe.js +3 -1
- package/src/commands/reportDashboard.js +21 -0
- package/src/commands/reportShareOfVoice.js +60 -0
- package/src/connectors/http.js +6 -3
- package/src/connectors/index.js +3 -1
- package/src/connectors/webflow.js +224 -0
- package/src/connectors/wordpress.js +226 -0
- package/src/detectors/geoReco.js +101 -0
- package/src/monitoring/alertDelivery.js +98 -0
- package/src/observations/common.js +10 -0
- package/src/observations/stance.js +261 -0
- package/src/reporting/dashboard.js +254 -0
- 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.
|
|
3
|
+
"version": "1.15.0",
|
|
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",
|
|
@@ -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.
|
|
10
|
+
version: 1.15.0
|
|
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]
|
|
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>
|
package/skill/commands/README.md
CHANGED
|
@@ -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
|
|
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]
|
|
97
|
-
|
|
98
|
-
changes
|
|
99
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
289
|
-
|
|
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,{
|
|
368
|
-
|
|
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': {
|
package/src/commands/connect.js
CHANGED
|
@@ -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
|
+
|
package/src/commands/context.js
CHANGED
|
@@ -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),
|
package/src/commands/delivery.js
CHANGED
|
@@ -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, {
|
|
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
|
-
|
|
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 };
|
package/src/commands/monitor.js
CHANGED
|
@@ -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
|
+
|
package/src/commands/observe.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|