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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ node_modules/
.pnp.js

# Build output
dist/
build/
*.tsbuildinfo
.cache/
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ For each `Top10Entry`, the synthesizer:
the reference list.
3. **Generates original prose** per section via a **cost-guarded, pluggable AI
provider** (deterministic → Ollama → OpenAI), with a deterministic fallback
that is itself a complete, publishable article.
that remains in the Ardur voice and is held for editorial review when AI
generation is unavailable.
4. **Assembles** the prose into the in-app `ArticleBlock[]` render model.
5. **Proves provenance** — every factual claim is mapped to the sources that
support it; ungrounded claims are dropped.
Expand All @@ -37,12 +38,8 @@ For each `Top10Entry`, the synthesizer:
bodies. Anything that fails the gate degrades to a stricter deterministic
article and records a warning. **It never aborts the cycle.**

The output is an `ArticleArtifact` — one `SynthesizedArticle` per Top-10 entry —
that the ardur.ai app renders directly.

> **This repo is a design spec + scaffold.** The synthesis logic is intentionally
> **not implemented**. See [`docs/spec.md`](./docs/spec.md) for the full design
> and [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the pipeline-wide contract.
The output is an `ArticleArtifact` with published `articles` and a separate
`heldArticles` editorial queue for deterministic or ungrounded fallbacks.

## Baked-in guarantees

Expand All @@ -62,8 +59,9 @@ npm install
npm run typecheck # tsc --noEmit
npm test # node --test (deterministic, zero network)
npm run build # tsc -> dist/
npm run test:package # pack + fresh consumer import smoke

# once implemented:
# run deterministic synthesis against local artifacts:
ARDUR_AI_PROVIDER=deterministic npm run synthesize \
--top10 data/runtime/top10.json \
--aggregation data/runtime/aggregation.json \
Expand All @@ -85,10 +83,12 @@ const articles: ArticleArtifact = await runSynthesis({
});
```

`runSynthesis` returns one `SynthesizedArticle` per `Top10Entry`, each carrying
`body: ArticleBlock[]`, `references`, `provenance`, `ai` (provider meta), and a
`legalNote`. See [`src/contracts.ts`](./src/contracts.ts) (the shared, vendored
contract — identical in all four repos).
`runSynthesis` returns a cycle artifact with `articles` for publishable,
AI-grounded output and `heldArticles` for deterministic or ungrounded fallbacks.
Each synthesized article carries `body: ArticleBlock[]`, `references`,
`provenance`, `ai` (provider meta), and a `legalNote`. See
[`src/contracts.ts`](./src/contracts.ts), the package shim over the shared
contract.

## Relationship to the existing ardur.ai system

Expand Down
94 changes: 94 additions & 0 deletions dist/assemble.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Article assembly rules — how 20–30 sources become ONE original piece.
*
* Mirrors the section order from ardur.ai's content-engine contract so the
* in-app render matches the existing site:
* Key Takeaway -> Why This Matters -> What Happened -> Builder View ->
* Open Questions -> Ardur Take
*
* The assembler:
* - dedups and ranks the cluster's members into a weave order (primary/paper
* sources first, then corroborating news), capping the reference list.
* - asks the provider for original prose per section, grounded in metadata only.
* - converts prose into the `ArticleBlock[]` in-app render model.
* - inserts at most one short (< 25-word) attributed quote where a primary
* source's exact wording is editorially necessary.
* - computes wordCount + readingTime and the source-trail block.
*
* VOICE: every section is assembled in the Ardur house voice
* ("GenZ-but-professional", see `style.ts` + docs/voice.md). The plan carries a
* per-section voice directive that is threaded into BOTH the provider prompt and
* the deterministic fallback templates, so a budget=0 article still reads in
* voice rather than as dry newswire. Voice never overrides the copyright,
* provenance, or render gates.
*/
import type { ArticleBlock, SynthesizedArticle, AggregatedItem, Top10Entry, ProviderMeta, ExtractedFact, ChartBlock } from './contracts.ts';
import { VOICE_STYLE, SECTION_VOICE, buildVoiceDirective, type VoiceStyle } from './style.ts';
import type { ArticleDraft } from './provider.ts';
export { VOICE_STYLE, SECTION_VOICE, buildVoiceDirective };
/** Canonical section ids, in render order. */
export type SectionId = 'key-takeaway' | 'why-this-matters' | 'what-happened' | 'builder-view' | 'open-questions' | 'ardur-take';
export interface SectionSpec {
id: SectionId;
heading: string;
/** Whether the section is required for a publishable article. */
required: boolean;
/** Soft word target — guides the provider, not a hard cap. */
targetWords: number;
}
/** The fixed section plan (order matters; it is the render contract). */
export declare const SECTION_PLAN: readonly SectionSpec[];
/** Default cap on how many references the source-trail block lists. */
export declare const MAX_REFERENCES = 30;
/** Minimum body length (words) for a publishable, non-`idea` article. */
export declare const MIN_BODY_WORDS = 150;
/** The ordered weave plan for one topic before prose is generated. */
export interface AssemblyPlan {
entry: Top10Entry;
/** Cluster members in weave order (most authoritative first). */
weave: AggregatedItem[];
/** Reference list after dedup + cap. */
references: AggregatedItem[];
sections: readonly SectionSpec[];
/** The voice this article is written in (defaults to VOICE_STYLE). */
voice: VoiceStyle;
/**
* Per-section voice directive (from `buildVoiceDirective`), threaded into both
* the provider prompt and the deterministic fallback so both paths sound the
* same. Keyed by SectionId.
*/
voiceDirectives: Record<SectionId, string>;
}
/**
* Build the deterministic weave/reference plan for a Top-10 entry from its
* cluster members. No prose yet — this decides ordering, dedup, caps, AND the
* per-section voice directives (from `style.ts`) that downstream prose obeys.
*/
export declare function planAssembly(entry: Top10Entry, clusterMembers: readonly AggregatedItem[], voice?: VoiceStyle): AssemblyPlan;
/**
* Convert generated section prose into the in-app `ArticleBlock[]` render model
* (headings, paragraphs, lists, an optional attributed quote, callouts). The
* prose is expected to already be on-voice; `lintVoice` (style.ts) is applied
* here as a final pass, downgrading off-voice phrasing to plainer wording
* (never blocking — accuracy/copyright gates run separately).
*/
export declare function toRenderBlocks(plan: AssemblyPlan, sectionProse: Record<SectionId, string>): ArticleBlock[];
/**
* Final assembly: combine plan + prose + provenance into a `SynthesizedArticle`,
* computing wordCount/readingTime and the reference list. Does NOT run the
* copyright gate — the caller (synthesize.ts) does that and may reject.
*/
export declare function assembleArticle(plan: AssemblyPlan, blocks: ArticleBlock[], draft: ArticleDraft, providerMeta: ProviderMeta, upstreamRunId: string, now: Date): SynthesizedArticle;
/**
* Build ChartBlock[] from the quantitative ExtractedFacts for a cluster.
* Only facts with a `quantity` field produce chart datapoints — no invented numbers.
*
* Groups facts by `quantity.metric` and builds one bar chart per metric that
* has ≥2 comparable datapoints. Single-datapoint metrics are skipped (a bar
* chart of one value is not informative).
*/
export declare function buildChartBlocks(facts: readonly ExtractedFact[], refs: readonly {
source: string;
url: string;
sourceDomain: string;
}[]): ChartBlock[];
Loading
Loading