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
94 changes: 80 additions & 14 deletions apps/decodex/src/radar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const DEFAULT_SIGNALS_DIR: &str = "site/src/content/signals";
const DEFAULT_STABLE_LIMIT: usize = 0;
const DEFAULT_TAG_PREFIX: &str = "rust-v";
const RELEASE_DELTA_SCHEMA: &str = "release_delta/v1";
const SCHEMA_VERSION: i64 = 2;
const SCHEMA_VERSION: i64 = 3;
const SIGNAL_SCHEMA: &str = "signal_entry/v1";
const SOCIAL_CANDIDATE_SCHEMA: &str = "social_candidate/v1";
const SOCIAL_POST_SCHEMA: &str = "social_post/v1";
Expand Down Expand Up @@ -77,14 +77,8 @@ const SOCIAL_POST_WORTHINESS: &[&str] = &["block", "publish", "skip"];
const SOURCE_ITEM_KINDS: &[&str] = &["commit", "pull_request"];
const UPSTREAM_IMPACT_KINDS: &[&str] =
&["browser_observation", "changelog", "commit", "pull_request", "release", "signal"];
const UPSTREAM_REVIEW_ACTION_TYPES: &[&str] = &[
"linear_followup",
"none",
"signal_entry",
"social_candidate",
"social_post",
"upstream_impact",
];
const UPSTREAM_REVIEW_ACTION_TYPES: &[&str] =
&["linear_followup", "none", "signal_entry", "social_candidate", "upstream_impact"];
const UPSTREAM_REVIEW_NEXT_STEPS: &[&str] = &["ai_review_required"];
const UPSTREAM_REVIEW_PRIORITIES: &[&str] = &["critical", "high", "low", "normal"];
const UPSTREAM_SOURCE_STATES: &[&str] = &["closed", "commit_only", "merged", "open"];
Expand Down Expand Up @@ -155,6 +149,7 @@ const ARTIFACT_KINDS: &[&str] = &[
"ledger_export",
"release_delta",
"signal",
"social_candidate",
"social_post",
"upstream_impact",
];
Expand Down Expand Up @@ -3234,6 +3229,7 @@ fn initialize_ledger(connection: &Connection) -> crate::prelude::Result<()> {
'analysis',
'signal',
'upstream_impact',
'social_candidate',
'social_post',
'release_delta',
'archive_manifest',
Expand Down Expand Up @@ -3263,7 +3259,7 @@ fn initialize_ledger(connection: &Connection) -> crate::prelude::Result<()> {
",
)?;

migrate_artifact_link_social_kind(connection)?;
migrate_artifact_link_kinds(connection)?;

connection.execute(
"
Expand All @@ -3277,7 +3273,7 @@ fn initialize_ledger(connection: &Connection) -> crate::prelude::Result<()> {
Ok(())
}

fn migrate_artifact_link_social_kind(connection: &Connection) -> crate::prelude::Result<()> {
fn migrate_artifact_link_kinds(connection: &Connection) -> crate::prelude::Result<()> {
let table_sql = connection
.query_row(
"
Expand All @@ -3293,7 +3289,7 @@ fn migrate_artifact_link_social_kind(connection: &Connection) -> crate::prelude:
return Ok(());
};

if !table_sql.contains("social_draft") {
if table_sql.contains("social_candidate") && !table_sql.contains("social_draft") {
return Ok(());
}

Expand All @@ -3311,6 +3307,7 @@ fn migrate_artifact_link_social_kind(connection: &Connection) -> crate::prelude:
'analysis',
'signal',
'upstream_impact',
'social_candidate',
'social_post',
'release_delta',
'archive_manifest',
Expand Down Expand Up @@ -5196,6 +5193,9 @@ fn validate_social_candidate_source_refs(refs: Option<&Value>, errors: &mut Vec<
.into(),
);
}
if refs.get("urls").is_some_and(|urls| !is_https_string_array(urls)) {
errors.push("source_refs.urls must be a list of https URLs".into());
}
}

fn validate_social_candidate_decision(decision: Option<&Value>, errors: &mut Vec<String>) {
Expand Down Expand Up @@ -5837,6 +5837,15 @@ mod tests {
assert_errors(&candidate, ["source_refs must include upstream_reviews"]);
}

#[test]
fn social_candidate_rejects_non_https_source_urls() {
let mut candidate = valid_social_candidate();

candidate["source_refs"]["urls"] = serde_json::json!(["http://example.test"]);

assert_errors(&candidate, ["source_refs.urls must be a list of https URLs"]);
}

#[test]
fn accepts_valid_upstream_impact_and_rejects_bad_angle() {
let mut impact = valid_upstream_impact();
Expand Down Expand Up @@ -6088,7 +6097,7 @@ mod tests {
.expect("schema version should be readable");

assert_eq!(artifact_kind, "social_post");
assert_eq!(schema_version, "2");
assert_eq!(schema_version, "3");
}

#[test]
Expand Down Expand Up @@ -6167,6 +6176,63 @@ mod tests {
assert_eq!(artifact_kind, "social_post");
}

#[test]
fn ledger_artifact_link_records_social_candidate_after_schema_migration() {
let temp_dir = tempfile::tempdir().expect("temporary directory should be created");
let db_path = temp_dir.path().join("radar.sqlite3");
let social_candidate_path = temp_dir.path().join("candidate.json");
let connection =
rusqlite::Connection::open(&db_path).expect("temporary ledger should open");

connection
.execute_batch(
"
CREATE TABLE artifact_link (
repo TEXT NOT NULL,
subject_kind TEXT NOT NULL CHECK (subject_kind IN ('commit', 'pr')),
subject_id TEXT NOT NULL,
artifact_kind TEXT NOT NULL CHECK (
artifact_kind IN (
'bundle',
'analysis',
'signal',
'upstream_impact',
'social_post',
'release_delta',
'archive_manifest',
'ledger_export'
)
),
path TEXT NOT NULL,
sha256 TEXT NOT NULL,
size_bytes INTEGER NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (repo, subject_kind, subject_id, artifact_kind, path)
);
",
)
.expect("legacy artifact link schema should be created");

drop(connection);

fs::write(&social_candidate_path, r#"{"schema":"social_candidate/v1"}"#)
.expect("social candidate fixture should be written");
radar::ledger_bootstrap(&RadarLedgerBootstrapRequest { db_path: db_path.clone() })
.expect("ledger bootstrap should add social_candidate artifact kind");

let summary = radar::ledger_artifact_link(&RadarLedgerArtifactLinkRequest {
db_path: db_path.clone(),
repo: "openai/codex".into(),
subject_kind: "pr".into(),
subject_id: "22414".into(),
artifact_kind: "social_candidate".into(),
path: social_candidate_path,
})
.expect("social candidate artifact link should be recorded");

assert_eq!(summary.get("artifact_links"), Some(&1));
}

#[test]
fn builds_pr_bundle_from_fixture_payloads() {
let patch = format!("{} --config FEATURE_FLAG=1", "a".repeat(910));
Expand Down Expand Up @@ -6615,7 +6681,7 @@ mod tests {
"made_with_ai": true,
"image_template": "decodex_signal_card"
},
"media_refs": ["artifacts/social/x/images/openai-codex-pr-22414.png"]
"media_refs": ["https://x.com/decodexspace/status/1/photo/1"]
})
}
}
4 changes: 2 additions & 2 deletions artifacts/github/reviews/openai-codex-pr-25636.review.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
"reason": "The rename can affect Decodex Control Plane compatibility and should be tracked as an upstream impact."
},
{
"type": "social_post",
"reason": "The rename is useful to explain publicly for operators following Codex MultiAgentV2 tooling changes."
"type": "social_candidate",
"reason": "The rename is useful to consider publicly for operators following Codex MultiAgentV2 tooling changes; Publisher should decide the terminal social_post outcome."
},
{
"type": "linear_followup",
Expand Down
10 changes: 7 additions & 3 deletions artifacts/social/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ This directory stores checked-in Publisher artifacts for external social channel

- `x/posts/` holds `social_post/v1` publication, block, skip, and failure records for
X.
- `x/images/` holds generated media evidence when the image is committed.
- `x/images/` is legacy or explicit operator-approved sample storage only. Publisher
automation should not commit generated images by default; use X status/media URLs and
optional content hashes instead.

The governing contract is `docs/spec/social-publishing.md`. The primary publishing
account is `@decodexspace`; the controller account is `@hackink`.
Social candidates live under `artifacts/github/social-candidates/`; this directory holds
terminal Publisher outcomes. The governing publication contract is
`docs/spec/social-publishing.md`. The primary publishing account is `@decodexspace`;
the controller account is `@hackink`.
31 changes: 27 additions & 4 deletions dev/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ with the installable Decodex plugin under `plugins/decodex/`.

## Skill Map

Use these skills in order when turning upstream Codex activity into Decodex content or
follow-up work:
Use these skills as a pipeline when turning upstream Codex activity into Decodex
content or follow-up work:

1. `codex-upstream-triage`: read the deterministic upstream review queue or a selected
source window and group commits by PR when possible.
Expand All @@ -19,14 +19,35 @@ follow-up work:
PRs, release-delta artifacts, and already-published Decodex signals.
4. `github-signal`: turn the reviewed GitHub bundle and analysis result into the
`analysis_draft` JSON consumed by `decodex radar render-signal`.
5. `x-post-publisher`: turn evidence-backed Radar output into a low-frequency
5. `x-post-publisher`: consume social candidates whose
`decision.worthiness = "publish"` or explicit operator handoffs that name checked
Radar artifacts, then write a low-frequency
`social_post/v1` publication, block, skip, or failure record for `@decodexspace`.
6. `rate-limit-reset-watch`: review today's `@thsottiaux` X posts with AI semantic
judgment and refresh the homepage `reset_status/v1` artifact.

Use only the skills needed for the current artifact. Do not publish a social post just
because a signal exists.

## Pipeline Ownership

Only the upstream analysis stage should read upstream Codex source for behavior claims:

- `codex-upstream-triage` selects and groups source candidates.
- `codex-code-analysis` reads upstream PR, commit, file, or patch evidence and produces
the source-backed interpretation.

Downstream skills are artifact consumers. `codex-release-analysis`, `github-signal`,
`x-post-publisher`, and `x-post-quality-system` should start from validated
`upstream_review/v1`, `upstream_impact/v1`, `signal_entry/v1`, `release_delta/v1`,
`social_candidate/v1`, or `analysis_draft` evidence. If that evidence is missing or too
weak, they must return `upstream_analysis_required` for missing source-analysis
evidence or preserve a `social_candidate/v1` with `decision.worthiness = "defer"` or
`"skip"` instead of doing ad hoc source analysis.

Release and prerelease automations may use compare metadata to detect gaps, but filling
those gaps belongs back in the upstream analysis stage.

Default posture: track every upstream Codex commit as a possible evidence unit. Resolve
commits back to PRs when possible, decide whether the change matters to Decodex Control
Plane or the wider Codex community, and only then promote important, useful, or
Expand All @@ -35,7 +56,9 @@ post.

For upstream releases and prereleases, use `codex-release-analysis` as a rollup over
the accumulated commit/PR analysis. Codex prerelease notes are often too sparse to
explain what changed by themselves.
explain what changed by themselves, but a new prerelease checkpoint can still produce a
cautious `social_candidate/v1` for a `release_pulse` intro or `watch_note` preview when
release metadata, compare metadata, and caveats create real reader value.

Checked-in contracts for this workflow are `upstream_review_queue/v1`,
`upstream_review/v1`, `github_change_bundle/v1`, `analysis_draft`, `signal_entry/v1`,
Expand Down
11 changes: 9 additions & 2 deletions dev/skills/codex-code-analysis/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ evidence into a defensible interpretation, not to rewrite release notes.
This is a Decodex repository-development instruction surface, not an installable
Decodex plugin skill.

This is the only repo-local skill that should read upstream Codex source for behavior,
compatibility, or Publisher claims during recurring Radar automation. Downstream
release, signal, and publishing skills consume this skill's reviewed artifacts instead
of redoing the source pass.

## Read Before Analysis

- `docs/spec/github-change-bundle.md`
Expand All @@ -30,7 +35,7 @@ Decodex plugin skill.
This skill may produce an `upstream_review/v1` when Codex automation is processing the
continuous review queue. Keep ad hoc manual notes in-session unless they are promoted
into `upstream_review/v1`, `analysis_draft`, `upstream_impact/v1`, or
`social_post/v1`.
`social_candidate/v1`.

## Analysis Loop

Expand Down Expand Up @@ -92,7 +97,9 @@ Return an analysis note that can feed `github-signal`, `codex-release-analysis`,
- Publisher angle, if any
- confidence and caveats
- recommended next artifact: `none`, `analysis_draft` through `github-signal`,
`upstream_impact/v1`, or `social_post/v1`
`upstream_impact/v1`, or `social_candidate/v1`
- downstream consumer gates: which artifacts are safe to consume and which claims still
require source review

Keep the note shorter than the source patch. Explain the behavior path, not every
changed file.
Loading