Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d50ec3c
content(principles-sheet): owner pass consolidating the X-by-Y revision
axsaucedo Jul 31, 2026
e25b830
content(principles-sheet): apply review fixes to the X-by-Y pass
axsaucedo Jul 31, 2026
c389be8
content(principles): sync the ratified X-by-Y revision into the colle…
axsaucedo Jul 31, 2026
6044141
docs(conventions): record the frontmatter inline-markup standard
axsaucedo Jul 31, 2026
5d77963
content(oss-sheet): v3 research-grounded page specifications
axsaucedo Jul 31, 2026
ff6832d
Rebuild open source index as project portal
axsaucedo Aug 1, 2026
72c62eb
Build comprehensive KAOS flagship page
axsaucedo Aug 1, 2026
00dc60b
Add production ML and AI guidelines catalogues
axsaucedo Aug 1, 2026
5ceaae5
Build Kompute and XAI project detail pages
axsaucedo Aug 1, 2026
e6132f7
Merge branch 'oss-kaos' into oss-integrate
axsaucedo Aug 1, 2026
3940f13
Merge branch 'oss-projects' into oss-integrate
axsaucedo Aug 1, 2026
e751273
Merge branch 'oss-lists' into oss-integrate
axsaucedo Aug 1, 2026
e8393e7
motion(oss): wire portal title morphs, fix rail jitter and affordance
axsaucedo Aug 1, 2026
a02ba0c
links: external non-institute links open in a new tab
axsaucedo Aug 1, 2026
e5fedac
Merge branch 'oss-extlinks' into redesign-astro
axsaucedo Aug 1, 2026
fc1a3d4
Fix OSS navigation and hero visuals
axsaucedo Aug 1, 2026
09737ff
Add shared animation window and highlighted code
axsaucedo Aug 1, 2026
7ff39fe
expt(hero): scroll-effect comparator with six URL-selectable variants
axsaucedo Aug 1, 2026
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ The ratified decisions in `.github/memory/2026-07-rebrand/` are authoritative. A
- Use a content collection when the site validates and iterates a set. Principles, partners, survey questions, and repository metrics are collections defined in `src/content.config.ts`.
- Page-owned prose, headings, links, lists, and configuration belong in that page's MDX front matter or body.
- Components arrange supplied content. They do not hide owner-editable page copy.
- Frontmatter copy fields render as escaped plain text by default. When a field genuinely needs inline emphasis, the standard is inline HTML in the field rendered through `set:html` at the consuming component; do not add markdown filters for frontmatter, and do not enable `set:html` on a field before its content actually carries markup. Scope stays inline (`<strong>`, `<em>`) on first-party copy fields only, never links or block markup. Not currently enabled anywhere.
- `composed: true` means the MDX owns its full-width composition. Otherwise `ProseLayout.astro` supplies the standard article shell.
- External HTTP(S) links outside `ethical.institute` and its subdomains open in a new tab with `target="_blank" rel="noopener noreferrer"`; internal, relative, anchor, and `mailto:` links do not. Markdown links are enforced by `src/plugins/rehype-external-links.mjs`; component-authored anchors must apply the same attributes.

The placement rule is: content lives with its owner; a separate file must be earned by two unrelated consumers, a validated set, or a genuinely large dataset.

Expand Down Expand Up @@ -68,6 +70,11 @@ All first-party client behaviour is TypeScript. Do not add first-party runtime J
| Survey title morph | Homepage report heading → survey explorer hero | `MorphPairs`, `ReportsSection`, `ArticleHero` |
| Initiative title morphs | Homepage maturity / security card titles → matching framework heroes | `MorphPairs`, `ReportsSection`, `ArticleHero`, `BaseLayout` |
| KAOS title morph | Homepage KAOS card title → KAOS page hero h1 | `MorphPairs`, `OpenSourceShowcase`, `ArticleHero` |
| OSS portal title morphs | `/open-source/` panel titles → project page heroes | `MorphPairs`, `ProjectPortal`, `ArticleHero` |
| Project portal activity | `/open-source/` project panels and production ML constellation | `ProjectPortal`, `CategoryConstellation`, `CanvasEngine` |
| Kompute sequence playback | Kompute detail sequence pipeline | `SequencePipeline` |
| Shared animation playback | Animated windows entering/leaving the viewport | `AnimationWindow`, slotted animation owner |
| Production ML atlas drift | Production ML catalogue category field | `CategoryAtlas` |
| Partner logo morph | Clicked marquee instance → matching directory logo | `MorphPairs`, `AffiliationMarquee`, `PartnerDirectory` |
| Principle directional slide | `/principles/NN/` prev/next → adjacent principle | `PrincipleLayout`, `Motion`, `tokens.css` |

Expand Down
3 changes: 2 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import { unified } from '@astrojs/markdown-remark';
import preact from '@astrojs/preact';
import rehypeExternalLinks from './src/plugins/rehype-external-links.mjs';
import rehypeSectionize from './src/plugins/rehype-sectionize.mjs';

export default defineConfig({
Expand Down Expand Up @@ -36,7 +37,7 @@ export default defineConfig({
'/privacypolicy.html': '/privacy/',
},
markdown: {
processor: unified({ rehypePlugins: [rehypeSectionize] }),
processor: unified({ rehypePlugins: [rehypeExternalLinks, rehypeSectionize] }),
},
integrations: [mdx(), preact(), sitemap()],
});
Binary file added public/images/oss/demo-monitoring-poster.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/oss/demo-monitoring.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/oss/demo-poster.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/oss/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/oss/kompute-architecture.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/oss/xai-imbalance.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/oss/xai-metrics.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/oss/xai-monitoring.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/oss/xai-pydata-talk.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
160 changes: 160 additions & 0 deletions scripts/parse-awesome-lists.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { spawnSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const outputPath = path.join(root, 'src/content/oss-catalogues.json');

const repositories = {
productionMl: 'EthicalML/awesome-production-machine-learning',
aiGuidelines: 'EthicalML/awesome-artificial-intelligence-regulation',
};

// Refresh the committed offline snapshot with: node scripts/parse-awesome-lists.mjs
function fetchReadme(repository) {
for (const branch of ['master', 'main']) {
const url = `https://raw.githubusercontent.com/${repository}/${branch}/README.md`;
const result = spawnSync('curl', ['-fsSL', '--retry', '1', url], {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
});
if (result.status === 0 && result.stdout.trim()) return { branch, text: result.stdout, url };
}
throw new Error(`Could not fetch README for ${repository} from master or main.`);
}

function sliceBetween(text, start, end) {
const startIndex = text.indexOf(start);
const endIndex = text.indexOf(end, startIndex + start.length);
if (startIndex < 0 || endIndex < 0) throw new Error(`Missing README boundary: ${start} / ${end}`);
return text.slice(startIndex, endIndex);
}

function markdownLinks(text) {
return [...text.matchAll(/\[([^\]]+)\]\(#([^)]+)\)/g)].map(([, label, anchor]) => ({
label: label.trim(),
anchor: `#${anchor}`,
}));
}

function sections(text, headingLevel) {
const lines = text.split(/\r?\n/);
const heading = new RegExp(`^#{${headingLevel}}\\s+(.+?)\\s*$`);
const results = [];
let current;

for (const line of lines) {
const match = line.match(heading);
if (match) {
current = { heading: match[1], count: 0 };
results.push(current);
} else if (current && /^\s*[*+-]\s+\[/.test(line)) {
current.count += 1;
}
}
return results;
}

function splitLeadingEmoji(label) {
const match = label.match(/^(\S+)\s+(.+)$/u);
if (!match) throw new Error(`Expected an emoji before label: ${label}`);
return { emoji: match[1], name: match[2] };
}

function splitTrailingEmoji(label) {
const match = label.match(/^(.+?)\s+(\S+)$/u);
if (!match) throw new Error(`Expected an emoji after label: ${label}`);
return { name: match[1], emoji: match[2] };
}

function zipLinksAndCounts(links, counts, parseLabel) {
if (links.length !== counts.length) {
throw new Error(
`README structure mismatch: found ${links.length} links and ${counts.length} sections.`,
);
}
const used = new Set();
const normalise = (value) =>
value
.toLowerCase()
.replaceAll('&', 'and')
.replaceAll(/[^a-z0-9]+/g, ' ')
.trim();

return links.map(({ label, anchor }, index) => {
const parsed = parseLabel(label);
let countIndex = counts.findIndex(
({ heading }, candidate) =>
!used.has(candidate) && normalise(heading) === normalise(parsed.name),
);
if (countIndex < 0 && !used.has(index)) countIndex = index;
if (countIndex < 0) throw new Error(`Could not match README section for ${parsed.name}.`);
used.add(countIndex);
return { ...parsed, count: counts[countIndex].count, anchor };
});
}

const production = fetchReadme(repositories.productionMl);
const productionLinks = markdownLinks(
sliceBetween(
production.text,
'## Quick links to sections on this page',
'## Contributing to the list',
),
);
const productionSections = sections(
sliceBetween(production.text, '# Main Content', '# Other Awesome Lists'),
2,
);
const categories = zipLinksAndCounts(productionLinks, productionSections, splitLeadingEmoji);

const regulation = fetchReadme(repositories.aiGuidelines);
const areaLinks = markdownLinks(
sliceBetween(regulation.text, '### National Regulation by Economic Area', '### Other Sections'),
);
const areaSections = sections(
sliceBetween(
regulation.text,
'# Regulation and Policy',
'# High Level Frameworks and Principles',
),
2,
);
const areas = zipLinksAndCounts(areaLinks, areaSections, splitTrailingEmoji);

const themeLinks = markdownLinks(
sliceBetween(regulation.text, '### Other Sections', '## Other relevant resources'),
);
const themeSections = sections(
regulation.text.slice(regulation.text.indexOf('# High Level Frameworks and Principles')),
1,
);
const themes = zipLinksAndCounts(themeLinks, themeSections, splitLeadingEmoji);

if (categories.length !== 24 || areas.length !== 15 || themes.length !== 6) {
throw new Error(
`Unexpected catalogue sizes: ${categories.length} categories, ${areas.length} areas, ${themes.length} themes.`,
);
}

const snapshot = {
productionMl: {
repository: `https://github.com/${repositories.productionMl}`,
source: production.url,
branch: production.branch,
categories,
},
aiGuidelines: {
repository: `https://github.com/${repositories.aiGuidelines}`,
source: regulation.url,
branch: regulation.branch,
areas,
themes,
},
};

writeFileSync(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`);
console.log(
`Wrote ${path.relative(root, outputPath)} (${categories.length} categories, ${areas.length} areas, ${themes.length} themes).`,
);
194 changes: 194 additions & 0 deletions src/components/AnimationWindow.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
---
interface Props {
title?: string;
chromeOnly?: boolean;
class?: string;
}

const { title, chromeOnly = false, class: className }: Props = Astro.props;
---

<animation-window class:list={[className, { 'is-chrome-only': chromeOnly }]} data-playing="false" data-chrome-only={chromeOnly ? '' : undefined}>
<header class="window-header">
<span class="traffic-lights" aria-hidden="true"><i></i><i></i><i></i></span>
<span class="window-title" data-window-title>{title}</span>
<span class="window-controls">
<slot name="controls" />
{
!chromeOnly && (
<button class="play-toggle" type="button" aria-label="Play animation" data-play-toggle>
<svg class="play-icon" aria-hidden="true" viewBox="0 0 12 12">
<path d="m3 2 7 4-7 4z" />
</svg>
<svg class="pause-icon" aria-hidden="true" viewBox="0 0 12 12">
<path d="M3 2h2v8H3zm4 0h2v8H7z" />
</svg>
</button>
)
}
</span>
</header>
<div class="window-content"><slot /></div>
</animation-window>

<!--
Playback contract: animated descendants read the host's data-playing value and
listen on their closest <animation-window> for animationwindow:play and
animationwindow:pause. A descendant may bubble animationwindow:complete when
its finite animation ends so the shared control returns to its play state.
-->
<script>
class AnimationWindowElement extends HTMLElement {
private controller = new AbortController();
private observer?: IntersectionObserver;
private playing = false;
private userPaused = false;

connectedCallback() {
this.controller = new AbortController();
const toggle = this.querySelector<HTMLButtonElement>('[data-play-toggle]');
if (!toggle) return;

this.setPlaying(false, false);
toggle.addEventListener(
'click',
() => {
this.userPaused = this.playing;
this.setPlaying(!this.playing);
},
{ signal: this.controller.signal },
);
this.addEventListener(
'animationwindow:complete',
() => {
this.userPaused = true;
this.setPlaying(false);
},
{ signal: this.controller.signal },
);

const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduced || !('IntersectionObserver' in window)) return;

this.observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
if (!this.userPaused) this.setPlaying(true);
} else {
this.setPlaying(false);
}
});
this.observer.observe(this);
}

disconnectedCallback() {
this.controller.abort();
this.observer?.disconnect();
}

private setPlaying(playing: boolean, announce = true) {
if (playing === this.playing && announce) return;
this.playing = playing;
this.dataset.playing = String(playing);
const toggle = this.querySelector<HTMLButtonElement>('[data-play-toggle]');
toggle?.setAttribute('aria-label', playing ? 'Pause animation' : 'Play animation');
if (announce) this.dispatchEvent(new CustomEvent(`animationwindow:${playing ? 'play' : 'pause'}`));
}
}

if (!customElements.get('animation-window')) customElements.define('animation-window', AnimationWindowElement);
</script>

<style>
animation-window {
background: var(--bg-inset);
border: 1px solid var(--hairline-card);
display: block;
overflow: hidden;
}
.window-header {
align-items: center;
background: rgba(255, 255, 255, 0.025);
border-bottom: 1px solid var(--hairline-card);
display: grid;
grid-template-columns: minmax(76px, 1fr) auto minmax(76px, 1fr);
min-height: 44px;
}
.traffic-lights {
display: flex;
gap: 7px;
padding-left: 15px;
}
.traffic-lights i {
background: var(--text-4);
border-radius: 50%;
height: 8px;
width: 8px;
}
.traffic-lights i:first-child {
background: var(--glitch-red);
}
.traffic-lights i:nth-child(2) {
background: var(--warn);
}
.traffic-lights i:last-child {
background: var(--accent);
}
.window-title {
color: var(--text-3);
font: var(--mono-10)/1 var(--font-mono);
letter-spacing: 0.08em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.window-controls {
align-items: center;
display: flex;
justify-content: flex-end;
min-width: 44px;
}
.play-toggle {
align-items: center;
align-self: stretch;
background: transparent;
border: 0;
border-left: 1px solid var(--hairline-card);
border-radius: var(--radius-control);
color: var(--text-2);
cursor: pointer;
display: flex;
justify-content: center;
min-height: 44px;
min-width: 44px;
padding: 0;
}
.play-toggle:hover,
.play-toggle:focus-visible {
background: var(--accent-wash-09);
color: var(--accent);
outline: none;
}
.play-toggle svg {
fill: currentColor;
height: 13px;
width: 13px;
}
.pause-icon,
animation-window[data-playing='true'] .play-icon {
display: none;
}
animation-window[data-playing='true'] .pause-icon {
display: block;
}
.window-content {
min-width: 0;
}
@media (max-width: 520px) {
.window-header {
grid-template-columns: minmax(68px, 1fr) auto minmax(68px, 1fr);
}
.traffic-lights {
padding-left: 12px;
}
}
</style>
Loading