agentlinkops 0.6.11 → 0.6.16
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/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/.mcp.json +0 -0
- package/CHANGELOG.md +35 -0
- package/LICENSE +0 -0
- package/NOTICE +0 -0
- package/PACK-ARTIFACT.json +16 -0
- package/PACKS.json +591 -0
- package/README.md +34 -48
- package/bin/agentlinkops.mjs +0 -0
- package/bin/linktrail.mjs +0 -0
- package/cli/adapters/csv.js +0 -0
- package/cli/adapters/sqlite-crm.js +0 -0
- package/cli/adapters/suppliers.js +1 -1
- package/cli/admission.js +0 -0
- package/cli/agent-setup.js +101 -17
- package/cli/agentlinkops.mjs +0 -0
- package/cli/args.js +0 -0
- package/cli/campaign-bridge.js +325 -0
- package/cli/campaign-outcomes.js +107 -0
- package/cli/catalog.js +47686 -16398
- package/cli/check.js +40 -10
- package/cli/chronology.js +39 -0
- package/cli/citation.js +0 -0
- package/cli/client.js +2 -1
- package/cli/cloud-error.js +0 -0
- package/cli/commands.js +17 -8
- package/cli/config.js +0 -0
- package/cli/connection.js +92 -18
- package/cli/contact-adoption.js +91 -0
- package/cli/context-mcp.js +0 -0
- package/cli/context.js +13 -2
- package/cli/delivery.js +0 -0
- package/cli/disavow.js +0 -0
- package/cli/doctor.js +15 -4
- package/cli/env.js +0 -0
- package/cli/fleet.js +30 -19
- package/cli/gsc-browser.js +0 -0
- package/cli/import.js +7 -1
- package/cli/index-observations.js +0 -0
- package/cli/index-submissions.js +0 -0
- package/cli/key-store.js +115 -0
- package/cli/ledger.js +1 -0
- package/cli/lifecycle.js +0 -0
- package/cli/linktrail.mjs +0 -0
- package/cli/local-result.js +41 -6
- package/cli/main.js +85 -20
- package/cli/mcp-client.js +2 -2
- package/cli/migration.js +308 -0
- package/cli/mirror.js +0 -0
- package/cli/mix.js +0 -0
- package/cli/native-lock.js +0 -0
- package/cli/oauth-credentials.js +0 -0
- package/cli/oauth-flow.js +0 -0
- package/cli/placement-projection.js +106 -0
- package/cli/platforms.js +0 -0
- package/cli/prompt.js +25 -0
- package/cli/public-http.js +168 -0
- package/cli/receipt-performance.js +0 -0
- package/cli/receipts.js +0 -0
- package/cli/report.js +51 -19
- package/cli/setup-plan.js +0 -0
- package/cli/skill.js +0 -0
- package/cli/state.js +76 -29
- package/cli/status.js +14 -18
- package/cli/sync-lock.js +0 -0
- package/cli/sync.js +23 -4
- package/package.json +7 -8
- package/references/citation-panels.md +2 -0
- package/references/cli.md +2 -2
- package/references/disavow.md +1 -1
- package/references/discovery-handoff.md +1 -1
- package/references/local-crm-draft-handoff.md +1 -1
- package/references/portable-ledger-handoff.md +11 -10
- package/references/recipes/catalog.json +263 -7
- package/references/recipes/command-notes.md +5 -3
- package/references/recipes/internal-link-improvement.md +25 -0
- package/references/recipes/placement-reconciliation.md +53 -1
- package/references/recipes/qualified-campaign-handoff.md +0 -0
- package/references/recipes/site-context-brief.md +0 -0
- package/references/recipes/sourced-linkable-asset.md +0 -0
- package/references/reports.md +0 -0
- package/scripts/agentlinkops.py +0 -0
- package/scripts/disavow.py +0 -0
- package/scripts/linktrail.py +0 -0
- package/scripts/migrations/001_crm.sql +0 -0
- package/scripts/migrations/002_cloud_events.sql +0 -0
- package/scripts/migrations/003_disavow.sql +0 -0
- package/scripts/migrations/004_placement_costs.sql +0 -0
- package/scripts/outreach-crm-draft.py +0 -0
- package/scripts/portable-handoff.py +17 -4
- package/scripts/report.py +0 -0
- package/shared/admission-contract.js +3 -2
- package/shared/admission-report-contract.js +3 -2
- package/shared/agent-clients.js +0 -0
- package/shared/analytics-history-contract.js +15 -0
- package/shared/campaign-outcome-contract.js +12 -0
- package/shared/campaign-prospect-contract.js +23 -0
- package/shared/citation-evidence-contract.js +0 -0
- package/shared/classification-contract.js +54 -0
- package/shared/contact-record-contract.js +27 -0
- package/shared/disavow-contract.js +37 -9
- package/shared/exec-grammar.js +0 -0
- package/shared/index-observation-contract.js +5 -4
- package/shared/internal-links-contract.js +143 -0
- package/shared/lifecycle-contract.js +0 -0
- package/shared/lifecycle-money.js +0 -0
- package/shared/link-category-contract.js +35 -0
- package/shared/link-category.js +138 -0
- package/shared/link-labels.js +26 -0
- package/shared/link-summary-contract.js +17 -0
- package/shared/monitoring-chronology-export.js +57 -0
- package/shared/monitoring-read-contract.js +41 -0
- package/shared/observation-event.js +1 -1
- package/shared/onboarding.js +20 -0
- package/shared/opportunity-record.js +0 -0
- package/shared/response-contract.js +41 -5
- package/shared/search-connection-contract.js +2 -1
- package/shared/table-page-contract.js +13 -0
- package/skills/agentlinkops-assets/SKILL-DEPS.json +290 -0
- package/skills/agentlinkops-assets/SKILL.md +3 -1
- package/skills/agentlinkops-assets/agents/openai.yaml +1 -1
- package/skills/agentlinkops-assets/references/portable/references/cli.md +220 -0
- package/skills/agentlinkops-assets/references/portable/references/disavow.md +73 -0
- package/skills/agentlinkops-assets/references/portable/references/reports.md +53 -0
- package/skills/agentlinkops-assets/references/portable/scripts/agentlinkops.py +676 -0
- package/skills/agentlinkops-assets/references/portable/scripts/disavow.py +207 -0
- package/skills/agentlinkops-assets/references/portable/scripts/migrations/001_crm.sql +45 -0
- package/skills/agentlinkops-assets/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
- package/skills/agentlinkops-assets/references/portable/scripts/migrations/003_disavow.sql +20 -0
- package/skills/agentlinkops-assets/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
- package/skills/agentlinkops-assets/references/portable/scripts/report.py +113 -0
- package/skills/agentlinkops-assets/references/portable/templates/campaigns.v1.json +65 -0
- package/skills/agentlinkops-campaigns/SKILL-DEPS.json +336 -0
- package/skills/agentlinkops-campaigns/SKILL.md +7 -5
- package/skills/agentlinkops-campaigns/agents/openai.yaml +1 -1
- package/skills/agentlinkops-campaigns/references/first-campaign.md +31 -0
- package/skills/agentlinkops-campaigns/references/mailbox-campaign.md +46 -0
- package/skills/agentlinkops-campaigns/references/portable/references/cli.md +220 -0
- package/skills/agentlinkops-campaigns/references/portable/references/disavow.md +73 -0
- package/skills/agentlinkops-campaigns/references/portable/references/reports.md +53 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/agentlinkops.py +676 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/disavow.py +207 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/001_crm.sql +45 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/003_disavow.sql +20 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/report.py +113 -0
- package/skills/agentlinkops-campaigns/references/portable/templates/campaigns.v1.json +65 -0
- package/skills/agentlinkops-connect/SKILL-DEPS.json +104 -0
- package/skills/agentlinkops-connect/SKILL.md +2 -0
- package/skills/agentlinkops-connect/agents/openai.yaml +1 -1
- package/skills/agentlinkops-connect/references/agentlinkops.md +164 -61
- package/skills/agentlinkops-connect/references/placement-workflows.md +19 -0
- package/skills/agentlinkops-crm/SKILL-DEPS.json +381 -0
- package/skills/agentlinkops-crm/SKILL.md +26 -9
- package/skills/agentlinkops-crm/agents/openai.yaml +1 -1
- package/skills/agentlinkops-crm/references/portable/references/cli.md +220 -0
- package/skills/agentlinkops-crm/references/portable/references/disavow.md +73 -0
- package/skills/agentlinkops-crm/references/portable/references/local-crm-draft-handoff.md +76 -0
- package/skills/agentlinkops-crm/references/portable/references/portable-ledger-handoff.md +51 -0
- package/skills/agentlinkops-crm/references/portable/references/reports.md +53 -0
- package/skills/agentlinkops-crm/references/portable/scripts/agentlinkops.py +676 -0
- package/skills/agentlinkops-crm/references/portable/scripts/disavow.py +207 -0
- package/skills/agentlinkops-crm/references/portable/scripts/migrations/001_crm.sql +45 -0
- package/skills/agentlinkops-crm/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
- package/skills/agentlinkops-crm/references/portable/scripts/migrations/003_disavow.sql +20 -0
- package/skills/agentlinkops-crm/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
- package/skills/agentlinkops-crm/references/portable/scripts/outreach-crm-draft.py +121 -0
- package/skills/agentlinkops-crm/references/portable/scripts/portable-handoff.py +197 -0
- package/skills/agentlinkops-crm/references/portable/scripts/report.py +113 -0
- package/skills/agentlinkops-crm/references/portable/templates/campaigns.v1.json +65 -0
- package/skills/agentlinkops-discovery/SKILL-DEPS.json +301 -0
- package/skills/agentlinkops-discovery/SKILL.md +4 -2
- package/skills/agentlinkops-discovery/references/portable/references/cli.md +220 -0
- package/skills/agentlinkops-discovery/references/portable/references/disavow.md +73 -0
- package/skills/agentlinkops-discovery/references/portable/references/discovery-handoff.md +32 -0
- package/skills/agentlinkops-discovery/references/portable/references/reports.md +53 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/agentlinkops.py +676 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/disavow.py +207 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/migrations/001_crm.sql +45 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/migrations/003_disavow.sql +20 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/report.py +113 -0
- package/skills/agentlinkops-discovery/references/portable/templates/campaigns.v1.json +65 -0
- package/skills/agentlinkops-discovery/references/saved-backlink-workflows.md +23 -0
- package/src/citations/adapter.js +0 -0
- package/src/citations/alert.js +0 -0
- package/src/citations/browser/chatgpt-citations.js +0 -0
- package/src/citations/browser/engine.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
- package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
- package/src/citations/browser/gen/lib/browser/display.js +0 -0
- package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
- package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
- package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
- package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
- package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
- package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
- package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
- package/src/citations/browser/gen/utils/agent-constants.js +0 -0
- package/src/citations/browser/providers.js +0 -0
- package/src/citations/browser/proxy-admission.js +0 -0
- package/src/citations/browser/shims/errors.js +0 -0
- package/src/citations/browser/shims/types.js +0 -0
- package/src/citations/browser/shims/utils.js +0 -0
- package/src/citations/contract.js +0 -0
- package/src/citations/egress-receipts.js +0 -0
- package/src/citations/evidence-integrity.js +0 -0
- package/src/citations/inventory.js +0 -0
- package/src/citations/login.js +0 -0
- package/src/citations/match.js +0 -0
- package/src/citations/panel-builder.js +0 -0
- package/src/citations/report.js +0 -0
- package/src/citations/runner.js +0 -0
- package/src/citations/stats.js +0 -0
- package/src/citations/sync.js +0 -0
- package/src/citations/watch.js +0 -0
- package/src/competitors/contract.js +17 -1
- package/src/competitors/domain-mix.js +0 -0
- package/src/context/ga4.js +0 -0
- package/src/context/gsc.js +0 -0
- package/src/context/site-profile.js +34 -121
- package/src/context/util.js +0 -0
- package/src/context-tool-registry.js +0 -0
- package/src/corpus/acquire.js +68 -32
- package/src/corpus/extract.js +147 -0
- package/src/corpus/frontier.js +151 -0
- package/src/corpus/seeds.js +0 -0
- package/src/discovery/contract.js +8 -0
- package/src/discovery/import.js +13 -1
- package/src/evidence-reference.js +0 -0
- package/src/index-observation.js +0 -0
- package/src/locator.js +0 -0
- package/src/mentions.js +193 -0
- package/src/page-acquisition/acquire.js +50 -0
- package/src/page-acquisition/bytes.js +138 -0
- package/src/page-acquisition/capture.js +11 -0
- package/src/page-acquisition/contracts.js +67 -0
- package/src/page-acquisition/documents.js +166 -0
- package/src/page-acquisition/http.js +307 -0
- package/src/page-acquisition/policy.js +20 -0
- package/src/page-acquisition/recovery.js +164 -0
- package/src/page-acquisition/render-failure.js +82 -0
- package/src/page-extraction/html.js +359 -0
- package/src/page-extraction/index.js +30 -0
- package/src/page-extraction/metadata.js +16 -0
- package/src/response-shaping.js +4 -3
- package/src/tool-views.js +23 -12
- package/src/verifier/fetch.js +4 -339
- package/src/verifier/index.js +81 -23
- package/src/verifier/observation-kind.js +0 -0
- package/src/verifier/page-quality.js +14 -0
- package/src/verifier/parser.js +2 -241
- package/src/verifier/readiness.js +64 -13
- package/src/verifier/render-evidence.js +25 -2
- package/src/verifier/robots.js +0 -0
- package/src/verifier/target.js +201 -0
- package/src/verifier/transition.js +18 -3
- package/src/verifier/url.js +31 -0
- package/src/watch-crawlability.js +144 -0
- package/templates/SITE.template.md +0 -0
- package/templates/campaigns.v1.json +0 -0
- package/skills/content-defingerprinting/SKILL.md +0 -29
- package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -102
- package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -26
- package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -52
- package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -21
- package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -96
- package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -432
- package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -86
- package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -460
- package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -651
- package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -27
- package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -212
- package/skills/internal-linking-optimizer/SKILL.md +0 -56
- package/skills/internal-linking-optimizer/references/source-prompt.md +0 -76
- package/skills/pre-publish-review/SKILL.md +0 -179
- package/skills/source-cited-content-builder/SKILL.md +0 -56
- package/skills/source-cited-content-builder/references/source-prompt.md +0 -19
- package/skills/source-cited-humanizer/SKILL.md +0 -20
|
@@ -1,460 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Zero-LLM statistical & stylometric AI-text detector.
|
|
3
|
-
|
|
4
|
-
Evaluates text for high-frequency AI cadence markers, sentence-length
|
|
5
|
-
burstiness variance, lexical diversity (MATTR), and structural uniformity.
|
|
6
|
-
Stdlib-only, no PyTorch or external model dependencies.
|
|
7
|
-
|
|
8
|
-
Exit codes:
|
|
9
|
-
0 clean (score < threshold)
|
|
10
|
-
1 suspicious / AI stylometric signals detected (score >= threshold)
|
|
11
|
-
2 bad input (missing file, binary data, bad args)
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import argparse
|
|
17
|
-
import math
|
|
18
|
-
import re
|
|
19
|
-
import sys
|
|
20
|
-
from collections import Counter
|
|
21
|
-
from dataclasses import dataclass, field
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
from typing import Any
|
|
24
|
-
|
|
25
|
-
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
26
|
-
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
27
|
-
|
|
28
|
-
from common import ( # noqa: E402
|
|
29
|
-
classify_finding_confidence,
|
|
30
|
-
emit_json,
|
|
31
|
-
read_text_input,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
DEFAULT_THRESHOLD = 0.65
|
|
35
|
-
MIN_SAMPLE_WORDS = 30
|
|
36
|
-
FULL_WEIGHT_WORDS = 100
|
|
37
|
-
|
|
38
|
-
# High-frequency formulaic transition markers, hedging verbs, and structural
|
|
39
|
-
# boilerplate commonly overrepresented in AI-generated text across frontier LLMs.
|
|
40
|
-
AI_PHRASE_PATTERNS: tuple[tuple[str, str, float], ...] = (
|
|
41
|
-
# (regex_pattern, human_label, weight)
|
|
42
|
-
(r"\bdelve(?:s|d)?\s+into\b", "delve into", 1.2),
|
|
43
|
-
(r"\ba\s+testament\s+to\b", "a testament to", 1.1),
|
|
44
|
-
(r"\brich\s+tapestry(?:\s+of)?\b", "rich tapestry", 1.3),
|
|
45
|
-
(r"\bplays?\s+a\s+(?:pivotal|crucial|vital|key)\s+role\b", "plays a pivotal/crucial role", 1.0),
|
|
46
|
-
(
|
|
47
|
-
r"\bin\s+(?:today'?s|the)\s+(?:(?:fast-paced|ever-evolving|digital|rapidly\s+changing)\s+)*(?:world|landscape|era|environment)\b",
|
|
48
|
-
"in today's fast-paced world/landscape",
|
|
49
|
-
1.4,
|
|
50
|
-
),
|
|
51
|
-
(
|
|
52
|
-
r"\bit\s+is\s+(?:important|essential|crucial|worth\s+noting)\s+to\s+(?:note|remember|consider|highlight)\b",
|
|
53
|
-
"it is important/crucial to note",
|
|
54
|
-
0.9,
|
|
55
|
-
),
|
|
56
|
-
(
|
|
57
|
-
r"\bnot\s+only\b[\w\s,]+\bbut\s+(?:also\s+)?(?:serves\s+to|acts\s+as|highlights)\b",
|
|
58
|
-
"not only ... but also serves to",
|
|
59
|
-
0.8,
|
|
60
|
-
),
|
|
61
|
-
(
|
|
62
|
-
r"\bserve(?:s|d)?\s+as\s+a\s+(?:beacon|reminder|catalyst|cornerstone)\b",
|
|
63
|
-
"serves as a beacon/catalyst/cornerstone",
|
|
64
|
-
1.1,
|
|
65
|
-
),
|
|
66
|
-
(
|
|
67
|
-
r"\bunderscore(?:s|d)?\s+the\s+(?:importance|need|significance)\b",
|
|
68
|
-
"underscores the importance/need",
|
|
69
|
-
0.9,
|
|
70
|
-
),
|
|
71
|
-
(
|
|
72
|
-
r"\bfoster(?:s|ing|ed)?\s+a\s+(?:sense|culture|deeper\s+understanding)\b",
|
|
73
|
-
"fosters a sense/culture",
|
|
74
|
-
0.9,
|
|
75
|
-
),
|
|
76
|
-
(
|
|
77
|
-
r"\bseamlessly\s+(?:integrates?|integrated|blends?|combine[sd]?)\b",
|
|
78
|
-
"seamlessly integrates/blends",
|
|
79
|
-
1.0,
|
|
80
|
-
),
|
|
81
|
-
(
|
|
82
|
-
r"\bnavigat(?:e|ing|es|ed)\s+the\s+(?:complexities|intricacies|nuances)\b",
|
|
83
|
-
"navigating the complexities/nuances",
|
|
84
|
-
1.0,
|
|
85
|
-
),
|
|
86
|
-
(r"\bmultifaceted\s+(?:nature|approach|landscape)\b", "multifaceted nature/approach", 1.0),
|
|
87
|
-
(r"\bharness(?:ing|ed|es)?\s+the\s+power\s+of\b", "harnessing the power of", 1.0),
|
|
88
|
-
(r"\ba\s+myriad\s+of\b", "a myriad of", 0.8),
|
|
89
|
-
(r"\bparadigm\s+shift\b", "paradigm shift", 0.9),
|
|
90
|
-
(r"\bholistic\s+(?:approach|view|perspective)\b", "holistic approach/perspective", 0.9),
|
|
91
|
-
(r"\bin\s+conclusion\b[,\s]", "in conclusion", 0.8),
|
|
92
|
-
(r"\bto\s+summarize\b[,\s]", "to summarize", 0.8),
|
|
93
|
-
(r"\bultimately\b[,\s]", "ultimately,", 0.6),
|
|
94
|
-
(r"\bfurthermore\b[,\s]", "furthermore,", 0.6),
|
|
95
|
-
(r"\bmoreover\b[,\s]", "moreover,", 0.6),
|
|
96
|
-
(r"\bas\s+an\s+ai\b", "as an AI", 1.5),
|
|
97
|
-
(r"\bi\s+hope\s+this\s+helps\b", "I hope this helps", 1.2),
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
RE_SENTENCE_SPLIT = re.compile(r"(?<=[.!?])\s+(?=[A-Z0-9\"'(\[])")
|
|
101
|
-
RE_WORDS = re.compile(r"\b[\w'-]+\b", re.UNICODE)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@dataclass
|
|
105
|
-
class MarkerMatch:
|
|
106
|
-
phrase: str
|
|
107
|
-
count: int
|
|
108
|
-
weight: float
|
|
109
|
-
samples: list[str] = field(default_factory=list)
|
|
110
|
-
|
|
111
|
-
def to_dict(self) -> dict[str, Any]:
|
|
112
|
-
return {
|
|
113
|
-
"phrase": self.phrase,
|
|
114
|
-
"count": self.count,
|
|
115
|
-
"weight": self.weight,
|
|
116
|
-
"samples": self.samples,
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@dataclass
|
|
121
|
-
class StylometryReport:
|
|
122
|
-
path: str
|
|
123
|
-
word_count: int
|
|
124
|
-
sentence_count: int
|
|
125
|
-
burstiness_cv: float | None
|
|
126
|
-
lexical_diversity: float
|
|
127
|
-
ai_ngram_density: float
|
|
128
|
-
matched_markers: list[dict[str, Any]]
|
|
129
|
-
score: float
|
|
130
|
-
confidence_level: str # CLEAN | LOW | MEDIUM | HIGH
|
|
131
|
-
status: str # ok | insufficient_length
|
|
132
|
-
findings: list[str] = field(default_factory=list)
|
|
133
|
-
notes: list[str] = field(default_factory=list)
|
|
134
|
-
|
|
135
|
-
def to_dict(self) -> dict[str, Any]:
|
|
136
|
-
return {
|
|
137
|
-
"path": self.path,
|
|
138
|
-
"word_count": self.word_count,
|
|
139
|
-
"sentence_count": self.sentence_count,
|
|
140
|
-
"burstiness_cv": round(self.burstiness_cv, 4)
|
|
141
|
-
if self.burstiness_cv is not None
|
|
142
|
-
else None,
|
|
143
|
-
"lexical_diversity": round(self.lexical_diversity, 4),
|
|
144
|
-
"ai_ngram_density": round(self.ai_ngram_density, 4),
|
|
145
|
-
"matched_markers": self.matched_markers,
|
|
146
|
-
"score": round(self.score, 4),
|
|
147
|
-
"confidence_level": self.confidence_level,
|
|
148
|
-
"status": self.status,
|
|
149
|
-
"findings": self.findings,
|
|
150
|
-
"findings_confidence": [classify_finding_confidence(f) for f in self.findings],
|
|
151
|
-
"notes": self.notes,
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def extract_sentences(text: str) -> list[str]:
|
|
156
|
-
"""Split text into sentences while ignoring blank lines and code block markers."""
|
|
157
|
-
clean_lines = []
|
|
158
|
-
in_code_block = False
|
|
159
|
-
for line in text.splitlines():
|
|
160
|
-
trimmed = line.strip()
|
|
161
|
-
if trimmed.startswith("```"):
|
|
162
|
-
in_code_block = not in_code_block
|
|
163
|
-
continue
|
|
164
|
-
if in_code_block or not trimmed:
|
|
165
|
-
continue
|
|
166
|
-
clean_lines.append(trimmed)
|
|
167
|
-
|
|
168
|
-
raw_text = "\n".join(clean_lines)
|
|
169
|
-
if not raw_text.strip():
|
|
170
|
-
return []
|
|
171
|
-
|
|
172
|
-
chunks = re.split(r"(?<=[.!?])\s+|\n+", raw_text)
|
|
173
|
-
sentences = [s.strip() for s in chunks if s.strip()]
|
|
174
|
-
return sentences
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def extract_words(text: str) -> list[str]:
|
|
178
|
-
"""Extract normalized alphanumeric word tokens."""
|
|
179
|
-
return [w.lower() for w in RE_WORDS.findall(text)]
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def compute_burstiness(sentences: list[str]) -> tuple[float, float, float | None]:
|
|
183
|
-
"""Compute mean sentence word length, standard deviation, and coefficient of variation (CV).
|
|
184
|
-
|
|
185
|
-
CV is ``None`` when it cannot be measured (no sentences, or fewer than two
|
|
186
|
-
with words): a one-or-zero-sample CV is undefined, and reporting it as 0.0
|
|
187
|
-
made the caller's tiering read "perfectly uniform" — the strongest
|
|
188
|
-
LLM-likeness signal — for text whose body yielded no sentences at all (#132).
|
|
189
|
-
"""
|
|
190
|
-
if not sentences:
|
|
191
|
-
return 0.0, 0.0, None
|
|
192
|
-
|
|
193
|
-
lengths = [len(extract_words(s)) for s in sentences]
|
|
194
|
-
lengths = [L for L in lengths if L > 0]
|
|
195
|
-
if len(lengths) < 2:
|
|
196
|
-
mean_len = float(lengths[0]) if lengths else 0.0
|
|
197
|
-
return mean_len, 0.0, None
|
|
198
|
-
|
|
199
|
-
mean_len = sum(lengths) / len(lengths)
|
|
200
|
-
variance = sum((x - mean_len) ** 2 for x in lengths) / (len(lengths) - 1)
|
|
201
|
-
std_dev = math.sqrt(variance)
|
|
202
|
-
cv = (std_dev / mean_len) if mean_len > 0 else 0.0
|
|
203
|
-
return mean_len, std_dev, cv
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def compute_mattr(words: list[str], window_size: int = 50) -> float:
|
|
207
|
-
"""Compute Moving-Average Type-Token Ratio (MATTR) across sliding windows."""
|
|
208
|
-
n = len(words)
|
|
209
|
-
if n == 0:
|
|
210
|
-
return 0.0
|
|
211
|
-
if n <= window_size:
|
|
212
|
-
return len(set(words)) / n
|
|
213
|
-
|
|
214
|
-
total_ttr = 0.0
|
|
215
|
-
num_windows = n - window_size + 1
|
|
216
|
-
# Sliding window counts
|
|
217
|
-
current_window = Counter(words[:window_size])
|
|
218
|
-
total_ttr += len(current_window) / window_size
|
|
219
|
-
|
|
220
|
-
for i in range(1, num_windows):
|
|
221
|
-
leaving_word = words[i - 1]
|
|
222
|
-
entering_word = words[i + window_size - 1]
|
|
223
|
-
|
|
224
|
-
current_window[leaving_word] -= 1
|
|
225
|
-
if current_window[leaving_word] == 0:
|
|
226
|
-
del current_window[leaving_word]
|
|
227
|
-
|
|
228
|
-
current_window[entering_word] += 1
|
|
229
|
-
total_ttr += len(current_window) / window_size
|
|
230
|
-
|
|
231
|
-
return total_ttr / num_windows
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def scan_ai_phrases(text: str) -> list[MarkerMatch]:
|
|
235
|
-
"""Find and tally high-frequency AI cadence phrases."""
|
|
236
|
-
matches: list[MarkerMatch] = []
|
|
237
|
-
for pattern, label, weight in AI_PHRASE_PATTERNS:
|
|
238
|
-
found_spans = []
|
|
239
|
-
for m in re.finditer(pattern, text, re.IGNORECASE):
|
|
240
|
-
found_spans.append(m.group(0))
|
|
241
|
-
if found_spans:
|
|
242
|
-
matches.append(
|
|
243
|
-
MarkerMatch(
|
|
244
|
-
phrase=label,
|
|
245
|
-
count=len(found_spans),
|
|
246
|
-
weight=weight,
|
|
247
|
-
samples=found_spans[:3],
|
|
248
|
-
)
|
|
249
|
-
)
|
|
250
|
-
return matches
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def score_text_stylometry(text: str, path: str = "<text>") -> StylometryReport:
|
|
254
|
-
"""Run full multi-dimensional stylometric analysis and return a structured report."""
|
|
255
|
-
words = extract_words(text)
|
|
256
|
-
word_count = len(words)
|
|
257
|
-
sentences = extract_sentences(text)
|
|
258
|
-
sentence_count = len(sentences)
|
|
259
|
-
|
|
260
|
-
findings: list[str] = []
|
|
261
|
-
notes: list[str] = []
|
|
262
|
-
|
|
263
|
-
# 1. Length Guard
|
|
264
|
-
if word_count < MIN_SAMPLE_WORDS:
|
|
265
|
-
marker_matches = scan_ai_phrases(text)
|
|
266
|
-
for m in marker_matches:
|
|
267
|
-
findings.append(f"AI phrase marker '{m.phrase}' found ({m.count}x)")
|
|
268
|
-
notes.append(
|
|
269
|
-
f"Sample contains {word_count} words; statistical stylometry is uncalibrated below {MIN_SAMPLE_WORDS} words"
|
|
270
|
-
)
|
|
271
|
-
return StylometryReport(
|
|
272
|
-
path=path,
|
|
273
|
-
word_count=word_count,
|
|
274
|
-
sentence_count=sentence_count,
|
|
275
|
-
burstiness_cv=None,
|
|
276
|
-
lexical_diversity=compute_mattr(words),
|
|
277
|
-
ai_ngram_density=0.0,
|
|
278
|
-
matched_markers=[m.to_dict() for m in marker_matches],
|
|
279
|
-
score=0.0,
|
|
280
|
-
confidence_level="CLEAN",
|
|
281
|
-
status="insufficient_length",
|
|
282
|
-
findings=findings,
|
|
283
|
-
notes=notes,
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
# 2. Metric Calculations
|
|
287
|
-
_, _, cv = compute_burstiness(sentences)
|
|
288
|
-
mattr = compute_mattr(words)
|
|
289
|
-
marker_matches = scan_ai_phrases(text)
|
|
290
|
-
|
|
291
|
-
# N-gram density: weighted marker instances per 100 words
|
|
292
|
-
total_marker_weight = sum(m.count * m.weight for m in marker_matches)
|
|
293
|
-
ngram_density = (total_marker_weight / (word_count / 100.0)) if word_count > 0 else 0.0
|
|
294
|
-
|
|
295
|
-
# 3. Component Sub-scores (0.0 to 1.0)
|
|
296
|
-
# Burstiness subscore: low CV (<0.35) is strongly characteristic of LLMs; high CV (>0.60) is human
|
|
297
|
-
burstiness_score: float | None
|
|
298
|
-
if cv is None:
|
|
299
|
-
# Fewer than two parseable sentences: burstiness is unmeasurable, not
|
|
300
|
-
# maximally LLM-like. The composite renormalizes over the remaining
|
|
301
|
-
# components instead of crediting the strongest signal (#132).
|
|
302
|
-
burstiness_score = None
|
|
303
|
-
elif cv < 0.25:
|
|
304
|
-
burstiness_score = 0.95
|
|
305
|
-
elif cv < 0.35:
|
|
306
|
-
burstiness_score = 0.80
|
|
307
|
-
elif cv < 0.45:
|
|
308
|
-
burstiness_score = 0.50
|
|
309
|
-
elif cv < 0.55:
|
|
310
|
-
burstiness_score = 0.25
|
|
311
|
-
else:
|
|
312
|
-
burstiness_score = 0.05
|
|
313
|
-
|
|
314
|
-
# N-gram subscore: >1.5 weighted matches per 100 words is very high
|
|
315
|
-
if ngram_density >= 2.0:
|
|
316
|
-
ngram_score = 1.0
|
|
317
|
-
elif ngram_density >= 1.0:
|
|
318
|
-
ngram_score = 0.75
|
|
319
|
-
elif ngram_density >= 0.5:
|
|
320
|
-
ngram_score = 0.45
|
|
321
|
-
elif ngram_density > 0:
|
|
322
|
-
ngram_score = 0.20
|
|
323
|
-
else:
|
|
324
|
-
ngram_score = 0.0
|
|
325
|
-
|
|
326
|
-
# Lexical uniformity subscore: LLMs cluster tightly around MATTR 0.65-0.78 for 50-word windows
|
|
327
|
-
diversity_score = 0.4 if 0.68 <= mattr <= 0.76 else 0.1
|
|
328
|
-
|
|
329
|
-
# 4. Composite Scoring & Small-Sample Dampening
|
|
330
|
-
if burstiness_score is None:
|
|
331
|
-
notes.append(
|
|
332
|
-
"Sentence burstiness unavailable (fewer than 2 parsed sentences — e.g. body wrapped in a code fence); composite renormalized over AI-phrase density and lexical diversity"
|
|
333
|
-
)
|
|
334
|
-
raw_composite = ((ngram_score * 0.45) + (diversity_score * 0.10)) / 0.55
|
|
335
|
-
else:
|
|
336
|
-
raw_composite = (burstiness_score * 0.45) + (ngram_score * 0.45) + (diversity_score * 0.10)
|
|
337
|
-
|
|
338
|
-
# Dampening factor: scales smoothly from 0.4 at MIN_SAMPLE_WORDS up to 1.0 at FULL_WEIGHT_WORDS
|
|
339
|
-
if word_count < FULL_WEIGHT_WORDS:
|
|
340
|
-
dampener = 0.4 + 0.6 * (
|
|
341
|
-
(word_count - MIN_SAMPLE_WORDS) / (FULL_WEIGHT_WORDS - MIN_SAMPLE_WORDS)
|
|
342
|
-
)
|
|
343
|
-
notes.append(
|
|
344
|
-
f"Sample word count ({word_count}) is in calibration range ({MIN_SAMPLE_WORDS}-{FULL_WEIGHT_WORDS}); score dampened by factor {dampener:.2f}"
|
|
345
|
-
)
|
|
346
|
-
else:
|
|
347
|
-
dampener = 1.0
|
|
348
|
-
|
|
349
|
-
final_score = min(1.0, max(0.0, raw_composite * dampener))
|
|
350
|
-
|
|
351
|
-
# 5. Classify Findings & Confidence Tiers
|
|
352
|
-
if marker_matches:
|
|
353
|
-
for m in marker_matches:
|
|
354
|
-
findings.append(f"AI cadence phrase '{m.phrase}' ({m.count}x)")
|
|
355
|
-
|
|
356
|
-
if cv is not None and cv < 0.35 and sentence_count >= 3:
|
|
357
|
-
findings.append(f"Unnaturally uniform sentence cadence (CV={cv:.2f} < 0.35)")
|
|
358
|
-
|
|
359
|
-
if ngram_density >= 1.0:
|
|
360
|
-
findings.append(f"Elevated AI formulaic transition density ({ngram_density:.2f}/100w)")
|
|
361
|
-
|
|
362
|
-
if final_score >= 0.75:
|
|
363
|
-
confidence = "HIGH"
|
|
364
|
-
elif final_score >= 0.50:
|
|
365
|
-
confidence = "MEDIUM"
|
|
366
|
-
elif final_score >= 0.25:
|
|
367
|
-
confidence = "LOW"
|
|
368
|
-
else:
|
|
369
|
-
confidence = "CLEAN"
|
|
370
|
-
|
|
371
|
-
return StylometryReport(
|
|
372
|
-
path=path,
|
|
373
|
-
word_count=word_count,
|
|
374
|
-
sentence_count=sentence_count,
|
|
375
|
-
burstiness_cv=cv,
|
|
376
|
-
lexical_diversity=mattr,
|
|
377
|
-
ai_ngram_density=ngram_density,
|
|
378
|
-
matched_markers=[m.to_dict() for m in marker_matches],
|
|
379
|
-
score=final_score,
|
|
380
|
-
confidence_level=confidence,
|
|
381
|
-
status="ok",
|
|
382
|
-
findings=findings,
|
|
383
|
-
notes=notes,
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
def print_human_stylometry_report(report: StylometryReport, explain: bool = False) -> None:
|
|
388
|
-
"""Print clean human-readable output to stdout."""
|
|
389
|
-
print(f"=== Stylometric AI-Text Report: {report.path} ===")
|
|
390
|
-
print(f"Status: {report.status}")
|
|
391
|
-
print(f"Confidence Level: {report.confidence_level}")
|
|
392
|
-
print(f"AI Probability: {report.score * 100:.1f}% (score: {report.score:.3f})")
|
|
393
|
-
print(f"Word Count: {report.word_count}")
|
|
394
|
-
print(f"Sentence Count: {report.sentence_count}")
|
|
395
|
-
cv_display = (
|
|
396
|
-
f"{report.burstiness_cv:.3f}"
|
|
397
|
-
if report.burstiness_cv is not None
|
|
398
|
-
else "n/a (fewer than 2 sentences)"
|
|
399
|
-
)
|
|
400
|
-
print(f"Sentence CV: {cv_display}")
|
|
401
|
-
print(f"Lexical Diversity: {report.lexical_diversity:.3f} (MATTR)")
|
|
402
|
-
print(f"AI Marker Density: {report.ai_ngram_density:.3f} / 100 words")
|
|
403
|
-
|
|
404
|
-
if report.findings:
|
|
405
|
-
print("\nFindings:")
|
|
406
|
-
for f in report.findings:
|
|
407
|
-
print(f" - {f}")
|
|
408
|
-
|
|
409
|
-
if explain and report.matched_markers:
|
|
410
|
-
print("\nMatched Phrases Detail:")
|
|
411
|
-
for m in report.matched_markers:
|
|
412
|
-
print(f" * {m['phrase']} (occurrences: {m['count']}, weight: {m['weight']})")
|
|
413
|
-
if m.get("samples"):
|
|
414
|
-
print(f' sample: "{m["samples"][0]}"')
|
|
415
|
-
|
|
416
|
-
if report.notes:
|
|
417
|
-
print("\nNotes:")
|
|
418
|
-
for n in report.notes:
|
|
419
|
-
print(f" * {n}")
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
def main() -> int:
|
|
423
|
-
p = argparse.ArgumentParser(
|
|
424
|
-
description=__doc__,
|
|
425
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
426
|
-
)
|
|
427
|
-
p.add_argument("path", nargs="?", default="-", help="Text file to score ('-' for stdin)")
|
|
428
|
-
p.add_argument(
|
|
429
|
-
"--threshold",
|
|
430
|
-
type=float,
|
|
431
|
-
default=DEFAULT_THRESHOLD,
|
|
432
|
-
help=f"Score threshold to trigger exit code 1 (default: {DEFAULT_THRESHOLD})",
|
|
433
|
-
)
|
|
434
|
-
p.add_argument("--json", action="store_true", help="Emit JSON output")
|
|
435
|
-
p.add_argument(
|
|
436
|
-
"--explain", action="store_true", help="Include detailed matched phrase breakdown"
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
args = p.parse_args()
|
|
440
|
-
|
|
441
|
-
# Read input
|
|
442
|
-
text = read_text_input(args.path)
|
|
443
|
-
if text is None:
|
|
444
|
-
return 2
|
|
445
|
-
|
|
446
|
-
input_label = "<stdin>" if args.path == "-" else args.path
|
|
447
|
-
report = score_text_stylometry(text, path=input_label)
|
|
448
|
-
|
|
449
|
-
if args.json:
|
|
450
|
-
emit_json(report.to_dict())
|
|
451
|
-
else:
|
|
452
|
-
print_human_stylometry_report(report, explain=args.explain)
|
|
453
|
-
|
|
454
|
-
if report.score >= args.threshold:
|
|
455
|
-
return 1
|
|
456
|
-
return 0
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if __name__ == "__main__":
|
|
460
|
-
sys.exit(main())
|