Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/browser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
with:
node-version: 24
- run: npm ci --prefix ..
- run: npx tsc --project tsconfig.e2e.json
- run: npm run docs:build --prefix ..
- run: npx e2e-web install chromium --with-deps
- run: npm run test:e2e
Expand Down
9 changes: 9 additions & 0 deletions .tegami/pangram-4-1-guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
packages:
"cargo:microck-pangram-cli": patch
"npm:@microck/pangram-cli": patch
---

## Changed

AI detection results in pretty, Markdown, and TUI views now caution that scans below 150 words are less certain, following Pangram 4.1's published interpretation guidance. Scores and machine-readable schemas are unchanged; undocumented dashboard uncertainty ranges are not fabricated.
8 changes: 8 additions & 0 deletions docs-app/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ It is an unofficial MIT-licensed client.

Content sent for analysis goes to Pangram. Local history is off by default.
Every billable MCP submission requires a caller-supplied cost ceiling.

## Pangram 4.1 interpretation guidance

Human-readable AI results warn when the returned segments total fewer than 150
words. This reflects the new short-scan guidance; it does not change the model's
score or imply that longer scans are proof of authorship. The announced dashboard
AI-percentage range is not yet part of the documented public API contract, so the
CLI does not manufacture one from segment confidence.
8 changes: 8 additions & 0 deletions docs-app/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,14 @@ It is an unofficial MIT-licensed client.
Content sent for analysis goes to Pangram. Local history is off by default.
Every billable MCP submission requires a caller-supplied cost ceiling.

## Pangram 4.1 interpretation guidance

Human-readable AI results warn when the returned segments total fewer than 150
words. This reflects the new short-scan guidance; it does not change the model's
score or imply that longer scans are proof of authorship. The announced dashboard
AI-percentage range is not yet part of the documented public API contract, so the
CLI does not manufacture one from segment confidence.

# Authentication reference

Source: https://pangram.micr.dev/docs/reference/authentication
Expand Down
8 changes: 8 additions & 0 deletions docs-app/public/markdown/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ It is an unofficial MIT-licensed client.

Content sent for analysis goes to Pangram. Local history is off by default.
Every billable MCP submission requires a caller-supplied cost ceiling.

## Pangram 4.1 interpretation guidance

Human-readable AI results warn when the returned segments total fewer than 150
words. This reflects the new short-scan guidance; it does not change the model's
score or imply that longer scans are proof of authorship. The announced dashboard
AI-percentage range is not yet part of the documented public API contract, so the
CLI does not manufacture one from segment confidence.
16 changes: 16 additions & 0 deletions docs-app/tsconfig.e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"incremental": false,
"types": [
"node"
]
},
"include": [
"e2e.config.ts",
"tests/**/*.ts"
],
"exclude": [
"node_modules"
]
}
24 changes: 20 additions & 4 deletions docs-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
Expand All @@ -14,8 +18,16 @@
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"paths": { "@/*": ["./*"] },
"plugins": [{ "name": "next" }]
"paths": {
"@/*": [
"./*"
]
},
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
Expand All @@ -24,5 +36,9 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
"exclude": [
"node_modules",
"tests",
"e2e.config.ts"
]
}
20 changes: 20 additions & 0 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -2475,3 +2475,23 @@ non-full `skills get` command. It also owns the exact `skills list` Markdown and
the two newline-terminated locator projections. Drift tests compare each
compiled command's stdout byte-for-byte with its owning generated or embedded
source.

## Pangram 4.1 interpretation guidance (2026-10-19)

Human-readable AI result projections (pretty, Markdown, and TUI Analyze/History)
show a short-text caution when the returned complete segment sequence has a
summed word count below 150. The returned segment count must equal the checked
sum of the AI, AI-assisted, and human segment counters. An absent sequence,
inconsistent segment counters, or overflowing count is unknown and does not
produce a short-text claim. The caution applies regardless
of classification or segment confidence; a high-confidence segment does not
cancel the document-length limitation. It does not alter scores, classifications,
exit codes, billing, saved canonical data, or JSON/JSONL/TOON schemas.

The 4.1 announcement establishes improved calibration, a dashboard estimated
AI-percentage range, a short-scan notice, and reported false-positive improvements.
It does not establish new public API request selectors, response-version values,
or range-field names. Until documented or authorized live conformance establishes
those details, the client retains its documented request/response contract and
does not derive a numerical interval from fractions or segment confidence.
See `docs/pangram-4-1-investigation.md` for the verified sources and remaining gap.
39 changes: 39 additions & 0 deletions docs/pangram-4-1-investigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Pangram 4.1: verified changes and client scope

Checked 2026-10-19. This is an implementation boundary, not a claim of complete
live API conformance or an independently verified accuracy benchmark.

## Announced changes

The [release post](https://x.com/max_spero_/status/2112249405684191350) announces
recalibrated confidence and a dashboard estimated AI-percentage range for
less-certain documents. The [short-scan follow-up](https://x.com/max_spero_/status/2112249407156388173)
adds a lower-certainty notice below 150 words. The [accuracy follow-up](https://x.com/max_spero_/status/2112249408888660063)
reports improvements to previously reported false positives. These are distinct
changes; the first screenshot alone was not the complete announcement.

## Implemented here

Pretty, Markdown and TUI AI results display a shared short-text caution based on
the returned segment word counts. No numerical range is fabricated. Existing
classification, fractions and per-segment confidence are preserved, as are
machine-readable output schemas. The model's accuracy changes are server-side;
the CLI does not attempt to reproduce or recalibrate the detector locally.

## Public API evidence and remaining gap

The [AI Detection API](https://docs.pangram.com/api-reference/ai-detection) still
shows the 4.0 result contract, including categorical segment confidence. The
[Models API](https://docs.pangram.com/api-reference/models) documents an ordered,
account-specific selector catalog; a model product name is not proof of a new
request selector. The public SDK at
[e8d737a](https://github.com/pangramlabs/pangram-sdk/commit/e8d737abf513c20808d3665025b76caa6ccfbdb9)
and public documentation at
[19c9200](https://github.com/pangramlabs/docs/commit/19c920036efe8344d75eb8dabba77083fda9eaf9)
do not yet define 4.1 range fields or a changed response-version string.

Accordingly, this patch does not invent a `pangram-4.1` selector, weaken the
version/shape guard, or claim that a dashboard range is available through the
public API. An actual 4.1 wire contract remains to be confirmed before those
changes can ship. No dashboard scraping, browser-session authentication,
provider credits, or public result links are used for verification.
2 changes: 2 additions & 0 deletions src/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ use uuid::{Uuid, Version};

mod bulk;
mod collection;
mod detection_guidance;
mod history;
mod model;

pub use bulk::*;
pub use collection::*;
pub use detection_guidance::short_text_caution;
pub use history::*;
pub use model::*;

Expand Down
104 changes: 104 additions & 0 deletions src/domain/detection_guidance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! Interpretation guidance shared by human-readable projections.
//!
//! This is not a new detector, confidence calibration, or numerical interval.

use super::AiDetectionResult;

const SHORT_TEXT_CAUTION: &str =
"Short text (under 150 words): detection is less certain. Treat this estimate cautiously.";

/// Returns the published short-scan caution for a complete returned segment
/// sequence. Missing evidence and overflowing counts remain unknown.
#[must_use]
pub fn short_text_caution(result: &AiDetectionResult) -> Option<&'static str> {
let segments = &result.segments;
let reported = result
.num_ai_segments
.checked_add(result.num_ai_assisted_segments)?
.checked_add(result.num_human_segments)?;
if segments.is_empty() || u64::try_from(segments.len()).ok()? != reported {
return None;
}
let words = segments.iter().try_fold(0_u64, |total, segment| {
total.checked_add(segment.word_count)
});
match words {
Some(0..=149) => Some(SHORT_TEXT_CAUTION),
_ => None,
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{Confidence, Fraction, NonEmptyString, Segment};

fn segment(words: u64) -> Segment {
Segment {
text: "synthetic".to_owned(),
label: NonEmptyString::new("Human Written").unwrap(),
ai_assistance_score: Fraction::new(0.0).unwrap(),
confidence: Confidence::High,
start_index: 0,
end_index: 9,
word_count: words,
token_length: words,
humanizer_score: None,
is_humanized: None,
}
}

#[test]
fn inconsistent_segment_counters_do_not_claim_a_short_document() {
let result = crate::domain::AiDetectionResult {
classification: crate::domain::AiClassification::Human,
headline: "Human".to_owned(),
prediction: "Human Written".to_owned(),
fraction_ai: Fraction::new(0.0).unwrap(),
fraction_ai_assisted: Fraction::new(0.0).unwrap(),
fraction_human: Fraction::new(1.0).unwrap(),
num_ai_segments: 0,
num_ai_assisted_segments: 0,
num_human_segments: 2,
segments: vec![segment(100)],
dashboard_link: None,
};
assert!(short_text_caution(&result).is_none());
let mut overflow = result.clone();
overflow.num_ai_segments = u64::MAX;
assert!(short_text_caution(&overflow).is_none());
let mut missing = result;
missing.num_human_segments = 0;
assert!(short_text_caution(&missing).is_none());
}

fn caution(segments: &[Segment]) -> Option<&'static str> {
short_text_caution(&AiDetectionResult {
classification: crate::domain::AiClassification::Human,
headline: "Human".to_owned(),
prediction: "Human Written".to_owned(),
fraction_ai: Fraction::new(0.0).unwrap(),
fraction_ai_assisted: Fraction::new(0.0).unwrap(),
fraction_human: Fraction::new(1.0).unwrap(),
num_ai_segments: 0,
num_ai_assisted_segments: 0,
num_human_segments: u64::try_from(segments.len()).unwrap(),
segments: segments.to_vec(),
dashboard_link: None,
})
}

#[test]
fn threshold_uses_the_document_total_not_individual_segment_lengths() {
assert!(caution(&[segment(149)]).is_some());
assert!(caution(&[segment(50), segment(99)]).is_some());
assert!(caution(&[segment(150)]).is_none());
assert!(caution(&[segment(75), segment(75)]).is_none());
}

#[test]
fn missing_or_overflowing_counts_do_not_claim_a_short_document() {
assert!(caution(&[]).is_none());
assert!(caution(&[segment(u64::MAX), segment(1)]).is_none());
}
}
3 changes: 3 additions & 0 deletions src/output/projection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,9 @@ fn write_ai_result<W: HumanWriter>(
) -> io::Result<()> {
writer.label_value("headline", &writer.escape(&result.headline))?;
writer.label_value("prediction", &writer.escape(&result.prediction))?;
if let Some(caution) = crate::domain::short_text_caution(result) {
writer.label_value("interpretation_note", caution)?;
}
writer.label_value(
"classification",
&writer.token(classification_label(result.classification)),
Expand Down
8 changes: 6 additions & 2 deletions src/tui/result-lines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ pub(crate) fn analysis_result_lines(
),
]));
lines.push(Line::styled(sanitize_single_line(&result.prediction), body));
if let Some(caution) = crate::domain::short_text_caution(result) {
lines.push(Line::styled(caution, muted));
}
// The fractions row doubles as the legend for the bar below
// it. Pangram's per-kind segment counts ride along muted as
// AI/AI-assisted/human; the segment list itself is the evidence.
Expand Down Expand Up @@ -634,8 +637,8 @@ mod tests {

assert_eq!(
text.len(),
20,
"segments read as paragraphs, then two facts rows each"
21,
"short-text caution precedes segment paragraphs and facts"
);
assert_eq!(
text,
Expand All @@ -645,6 +648,7 @@ mod tests {
"AI detection",
"Mixed - Mixed [31m authorship",
"The document contains mixed authorship.",
"Short text (under 150 words): detection is less certain. Treat this estimate cautiously.",
"AI 50.0% AI-assisted 25.0% Human 25.0% 2 segments (1/0/1)",
&format!("{}{}", "#".repeat(43), ".".repeat(17)),
"",
Expand Down
11 changes: 11 additions & 0 deletions tests/projection-contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,3 +624,14 @@ fn jsonl_rejects_an_empty_series() {
let result = render(OutputFormat::Jsonl, ColorPolicy::Plain, &[], &mut sink);
assert!(result.is_err(), "JSONL accepted zero envelopes");
}

#[test]
fn short_ai_results_include_an_interpretation_caution_in_human_formats_only() {
let envelope = success_envelope();
for format in [OutputFormat::Pretty, OutputFormat::Markdown] {
let text = render_string(format, ColorPolicy::Plain, std::slice::from_ref(&envelope));
assert!(text.contains("Short text (under 150 words): detection is less certain."));
}
let json = render_string(OutputFormat::Json, ColorPolicy::Plain, &[envelope]);
assert!(!json.contains("interpretation_note"));
}
1 change: 1 addition & 0 deletions tests/support/golden/markdown_success.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- **upstream_stage:** STAGE_SUCCESS
- **headline:** Human-written
- **prediction:** The document appears to be human-written.
- **interpretation_note:** Short text (under 150 words): detection is less certain. Treat this estimate cautiously.
- **classification:** human
- **fraction_ai:** 0
- **fraction_ai_assisted:** 0
Expand Down
1 change: 1 addition & 0 deletions tests/support/golden/pretty_success.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Checks
upstream_stage: STAGE_SUCCESS
headline: Human-written
prediction: The document appears to be human-written.
interpretation_note: Short text (under 150 words): detection is less certain. Treat this estimate cautiously.
classification: human
fraction_ai: 0
fraction_ai_assisted: 0
Expand Down
1 change: 1 addition & 0 deletions tests/support/golden/pretty_success_color.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
upstream_stage: STAGE_SUCCESS
headline: Human-written
prediction: The document appears to be human-written.
interpretation_note: Short text (under 150 words): detection is less certain. Treat this estimate cautiously.
classification: human
fraction_ai: 0
fraction_ai_assisted: 0
Expand Down
Loading
Loading