Skip to content
Open
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
4 changes: 4 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ on:
- "wc/**"
- "wc2026/**"
- "scripts/validate-pack.mjs"
- "scripts/source-artifact.mjs"
- "scripts/source-artifact.test.mjs"
- ".github/workflows/pages.yml"
workflow_dispatch:

Expand All @@ -33,6 +35,8 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Test connector source artifact contract
run: node scripts/source-artifact.test.mjs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate Pages deploy on source capture validation

For pushes to main that change packs/**, this Pages gate still only runs the unit test plus validate-pack; validatePackData does not inspect event.sources, while the new CLI that rejects truncated connector captures is scripts/validate-source-artifacts.mjs. Since the deploy job depends only on this workflow's validate job, an invalid connector capture can still be published to Pages even if the separate source-artifacts workflow fails, so add node scripts/validate-source-artifacts.mjs here or make validate-pack call it before deploy.

Useful? React with 👍 / 👎.

- name: Validate all packs
run: node scripts/validate-pack.mjs

Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/source-artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: source-artifacts

on:
push:
paths:
- "packs/**"
- "scripts/add-match-day.mjs"
- "scripts/source-artifact.mjs"
- "scripts/source-artifact.test.mjs"
- "scripts/validate-source-artifacts.mjs"
- ".github/workflows/source-artifacts.yml"
pull_request:
paths:
- "packs/**"
- "scripts/add-match-day.mjs"
- "scripts/source-artifact.mjs"
- "scripts/source-artifact.test.mjs"
- "scripts/validate-source-artifacts.mjs"
- ".github/workflows/source-artifacts.yml"

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Test source artifact contract
run: node scripts/source-artifact.test.mjs
- name: Validate source captures in all packs
run: node scripts/validate-source-artifacts.mjs
17 changes: 15 additions & 2 deletions docs/DAILY-AGENT-PROMPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ or unsourced scoreline is a public credibility failure.
> first — it explains why a mechanical "Scorers: …" feed makes the page *disprove* Causari, and
> defines exactly what a real `whyItMatters` / `nextWatchpoints` / typed-link set must contain.
> The prompt below now also runs `scripts/causal-quality.mjs`, the gate that FAILS templated data.
>
> **Connector evidence follows [`docs/SOURCE-ARTIFACT-CONTRACT.md`](./SOURCE-ARTIFACT-CONTRACT.md).**
> A truncated tool preview is routing context, not sufficient evidence for a public factual claim.

---

Expand All @@ -24,6 +27,10 @@ or unsourced scoreline is a public credibility failure.
other packs, or unrelated files.
4. **Follow the graph model** (see `docs/PACKS.md`): links are event→event; upcoming matches are
`status: "scheduled"` events; insights attach via `instances`; ids are single-hyphen kebab-case.
5. **Never silently trust truncation.** When evidence came through a connector, add explicit
`capture.mode: "connector"` and `capture.truncated: true|false`. If truncated, retain a full
checksummed artifact reference and read it (or independently verify the authoritative URL)
before publishing. If the full evidence cannot be retrieved, skip the claim.

---

Expand All @@ -37,13 +44,17 @@ Each run, do exactly this:
1. Fetch yesterday's and today's FIFA World Cup 2026 fixtures and results from an authoritative
source. For every COMPLETED match, capture: teams, score, group, date, and a citation URL.
If you cannot verify a result against a real source, skip it — never guess a score.
If a tool or connector returns a truncated preview, do not treat that preview as the full source:
retrieve the full artifact or verify the claim directly at the authoritative URL first.

2. Build a match-day input file `scripts/match-day.json` following `scripts/match-day.example.json`:
- "results": each completed match as an event. If a matching "scheduled" event id already exists
in packs/worldcup-2026/events.json, reuse that id so the script flips it to completed. Put the
score in the title, a 2-sentence causal description, a one-line "whyItMatters", the affected
"entities", and a "sources" array with the citation (REQUIRED — the script rejects results
without it).
without it). For connector-obtained evidence, include `capture` metadata that follows
docs/SOURCE-ARTIFACT-CONTRACT.md; truncated captures require a full artifact URI, SHA-256,
byte length, bounded preview, and retrieval hint.
- "scheduled": the next fixtures these results set up, as upcoming events.
- "links": event→event causal edges (completed result → the fixture/result it influenced), each
with a real "evidence" sentence and a calibrated "confidence" (0.6–0.85 typical; reserve
Expand All @@ -53,7 +64,7 @@ Each run, do exactly this:

3. Apply + validate:
node scripts/add-match-day.mjs scripts/match-day.json
node scripts/validate-pack.mjs worldcup-2026 # structure
node scripts/validate-pack.mjs worldcup-2026 # structure + source artifact safety
node scripts/causal-quality.mjs worldcup-2026 # semantic causal gate
If any command exits non-zero, fix the input and retry. Do NOT proceed on failure.
(add-match-day.mjs already runs the causal-quality gate in memory before writing.)
Expand All @@ -75,6 +86,8 @@ sourced update beats a large speculative one.
- `add-match-day.mjs` validates the merged pack **in memory and writes nothing on error** — a bad
input can't corrupt the pack on disk.
- The honesty gate (`sources` required for completed results) is enforced in code, not just prose.
- Connector evidence must declare whether it was truncated; truncated previews cannot pass without
a retained, checksummed full-artifact reference.
- CI (`validate-packs`) re-checks on every push as a backstop, and the gated Pages deploy
(`.github/workflows/pages.yml`) will **not republish** the live data if validation fails — so the
public visual keeps serving the last-good pack even if a bad commit somehow lands.
10 changes: 9 additions & 1 deletion docs/LIVE-UPDATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ whole pack in memory, writing nothing if anything is broken**. The scheduled age

1. **Write the day's input** — copy [`scripts/match-day.example.json`](../scripts/match-day.example.json)
to `scripts/match-day.json` and fill in the day's **real, sourced** results, next fixtures, and links.
Every completed result needs a `sources` citation (the script rejects results without one).
Every completed result needs a `sources` citation (the script rejects results without one). If the
citation was obtained through a connector, follow [SOURCE-ARTIFACT-CONTRACT.md](SOURCE-ARTIFACT-CONTRACT.md):
declare truncation explicitly and retain a checksummed full artifact when a response was truncated.
2. **Apply + validate** (writes only if clean):
```bash
node scripts/add-match-day.mjs scripts/match-day.json
node scripts/source-artifact.test.mjs
node scripts/validate-pack.mjs worldcup-2026
```
If either exits non-zero, fix the input and retry — never commit a broken pack.
Expand Down Expand Up @@ -85,6 +88,11 @@ A `completed` result is a factual claim. Every one must carry a source, e.g.:

Until a result is sourced, keep the pack README's "illustrative / proof-of-concept" banner. Calibrated, sourced data is the whole brand promise — do not publish invented scorelines as fact.

A connector preview is not automatically the full source. When `capture.mode` is `connector`,
`capture.truncated` must be explicit. A truncated capture is valid only when the complete payload is
retained behind a checksummed artifact reference and the updater reads that artifact (or verifies the
authoritative URL) before publishing.

---

## Graph rules recap (see docs/PACKS.md)
Expand Down
95 changes: 95 additions & 0 deletions docs/SOURCE-ARTIFACT-CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Source Artifact Safety Contract

Status: v1
Scope: evidence obtained through tools/connectors before it becomes public Causari data.

## Problem

Connector and tool responses may be truncated to fit an agent context. A preview is useful for routing, but it is unsafe as the sole basis for a public factual claim because omitted rows or fields may change the conclusion.

Causari therefore distinguishes:

```text
direct, independently retrievable citation
vs.
connector capture with explicit completeness metadata
```

The pack remains deterministic. This contract does not add another agent loop; it only controls what evidence an updater may trust.

## Rules

1. A normal authoritative URL may remain a legacy source object with `type`, `citation`, and `url`.
2. Evidence obtained through a connector should include `capture.mode: "connector"`.
3. Connector evidence must state `truncated: true|false`. Missing means unknown and fails validation.
4. A truncated response must retain a full artifact with:
- stable URI or content location;
- SHA-256 checksum;
- full payload byte length;
- bounded preview;
- retrieval instructions.
5. An agent must read the full artifact or independently verify the claim at the authoritative URL before publishing.
6. The artifact itself should not be copied into `events.json`; the pack stores only provenance metadata.
7. Secrets, access tokens, cookies, and private headers must never be retained in previews or artifacts.

## Source examples

### Direct source

```json
{
"type": "official",
"citation": "FIFA — World Cup 2026 match centre",
"url": "https://www.fifa.com/..."
}
```

### Complete connector response

```json
{
"type": "official",
"citation": "FIFA — World Cup 2026 match centre",
"url": "https://www.fifa.com/...",
"capture": {
"mode": "connector",
"truncated": false,
"capturedAt": "2026-07-15T01:05:00Z"
}
}
```

### Truncated connector response with retained artifact

```json
{
"type": "official",
"citation": "FIFA — World Cup 2026 match centre",
"url": "https://www.fifa.com/...",
"capture": {
"mode": "connector",
"truncated": true,
"capturedAt": "2026-07-15T01:05:00Z",
"artifact": {
"uri": "artifact://connector/fifa-2026-07-15.json",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"byteLength": 120345,
"preview": "{\"matches\":[...]}",
"retrievalHint": "Read the stored JSON artifact before verifying scoreline claims."
}
}
}
```

## Validation

`validate-pack.mjs` calls `validateSourceCapture` for every event source. Existing direct citations remain backward compatible. Only a source that opts into connector capture is subject to the new completeness rules.

Run:

```bash
node scripts/source-artifact.test.mjs
node scripts/validate-pack.mjs
```

This contract protects provenance completeness. It does not prove that a source is authoritative or that the causal interpretation is correct; the honesty and causal-quality gates still apply.
13 changes: 12 additions & 1 deletion scripts/add-match-day.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { validatePackData } from './validate-pack.mjs';
import { validateEventSources } from './validate-source-artifacts.mjs';
import { assessCausalQuality } from './causal-quality.mjs';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
Expand Down Expand Up @@ -106,7 +107,17 @@ if (errors.length > 0) {
for (const e of errors) console.error(` - ${e}`);
process.exit(1);
}
// (b) causal-quality gate — the layer must be real, not templated. Catches a

// (b) source-artifact validation — connector truncation must be explicit and
// any truncated preview must retain a checksummed full-payload reference.
const sourceErrors = validateEventSources(merged.events, packId);
if (sourceErrors.length > 0) {
console.error(`✗ ${sourceErrors.length} source artifact error(s) — nothing written:`);
for (const e of sourceErrors) console.error(` - ${e}`);
process.exit(1);
}

// (c) causal-quality gate — the layer must be real, not templated. Catches a
// "Scorers: …" whyItMatters, a single-relationship graph, a backwards scoreline,
// a scheduled event carrying a result, an unresolved round/team, etc. This is
// what keeps the daily editorial honest even when structure is fine.
Expand Down
93 changes: 93 additions & 0 deletions scripts/source-artifact.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env node

const SHA256 = /^[a-f0-9]{64}$/i;

function isObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}

function isNonNegativeInteger(value) {
return Number.isInteger(value) && value >= 0;
}

function validIsoDate(value) {
return isNonEmptyString(value) && !Number.isNaN(Date.parse(value));
}

function validateArtifact(artifact, label) {
const errors = [];

if (!isObject(artifact)) {
return [`${label}.artifact must be an object when connector output is truncated`];
}
if (!isNonEmptyString(artifact.uri)) {
errors.push(`${label}.artifact.uri must point to the full stored payload`);
}
if (!isNonEmptyString(artifact.sha256) || !SHA256.test(artifact.sha256)) {
errors.push(`${label}.artifact.sha256 must be a 64-character SHA-256 hex digest`);
}
if (!isNonNegativeInteger(artifact.byteLength)) {
errors.push(`${label}.artifact.byteLength must be a non-negative integer`);
}
if (!isNonEmptyString(artifact.preview)) {
errors.push(`${label}.artifact.preview must be a non-empty bounded preview`);
}
if (!isNonEmptyString(artifact.retrievalHint)) {
errors.push(`${label}.artifact.retrievalHint must explain how to read the full payload`);
}

return errors;
}

/**
* Validate optional evidence-capture metadata attached to an event source.
*
* Legacy/direct URL citations do not need `capture`. Once evidence is obtained
* through a connector, `capture.mode = "connector"` makes truncation explicit.
* A truncated connector response is invalid unless the full payload is stored
* and addressable by a checksummed artifact reference.
*/
export function validateSourceCapture(source, label = 'source') {
const errors = [];

if (!isObject(source)) {
return [`${label} must be an object`];
}

const capture = source.capture;
if (capture === undefined) return errors;
if (!isObject(capture)) return [`${label}.capture must be an object`];

if (!['direct', 'connector'].includes(capture.mode)) {
errors.push(`${label}.capture.mode must be "direct" or "connector"`);
return errors;
}

if (capture.capturedAt !== undefined && !validIsoDate(capture.capturedAt)) {
errors.push(`${label}.capture.capturedAt must be an ISO date-time`);
}

if (capture.mode === 'direct') {
if (capture.truncated === true) {
errors.push(`${label}.capture: direct evidence cannot claim a truncated connector payload`);
}
return errors;
}

if (typeof capture.truncated !== 'boolean') {
errors.push(`${label}.capture.truncated must be an explicit boolean for connector evidence`);
return errors;
}

if (capture.truncated === true) {
errors.push(...validateArtifact(capture.artifact, label));
} else if (capture.artifact !== undefined) {
errors.push(...validateArtifact(capture.artifact, label));
}

return errors;
}
Loading
Loading