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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ bench/

# edits applied through ccc's edit tools, read back by `ccc prompts`
/.ccc/edits.jsonl

# timeline steps the ccc servers on this project share
/.ccc/timeline.jsonl
47 changes: 38 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,35 @@

[![Release Collateral Code Check](https://github.com/colwill/ccc/actions/workflows/ccc-release.yaml/badge.svg)](https://github.com/colwill/ccc/actions/workflows/ccc-release.yaml)

<p align="center" style="width:100%"><a href="https://github.com/colwill/ccc" target="_blank"><img src="visual.png" alt="Collateral Code Check Visualiser Example"></a></p>

# Collateral Code Check (`ccc`)

Collateral Code Check tells you, and your AI agent, what a change touches before you commit it - the functions, tests, services and cross-service contracts it reaches - by:
What is becoming obvious is that understanding the changes being made by LLMs/Agents to projects is an increasingly complex problem.

- highlighting which tests will be ran with your changes
`ccc` reduces that problem by providing tools to understand code, visualise changes for humans and allow LLMs/Agents to make the changes they need to with significant oversight by the human in charge.

## So what is ccc?

Collateral Code Check - (collateral as-in collateral damage) shows everything a change touches (before you commit it!) visually in your IDE, in your pipeline or anywhere else you need to see it. This includes; functions, tests, services and cross-service contracts it reaches.

It does this by:

- Highlighting what pipeline tests run with your changes

- showing if your change violates language linting rules
- Showing if your change violates language linting rules

- Showing new or modified cross-service calls before you commit

- showing when you create or modify cross-service calls before you commit
- Provides agents/models with accurate real-time insights

- providing language models with accurate real-time insights into your changes
- Provides agents/models with restricted edit controls (no sed, sudo etc)

- triggering specific testing tools based on your changes
- Visualise changes from agents/models before they are applied with the visualiser.

- making your agent's changes for it - a project-wide rename, a dead-code delete, a new function beside an old one - as one confined, previewed, revertible call that costs a fraction of the tokens read-then-edit does with an extra layer of safety ([how much](#edits-go-through-ccc))
Additionally, benefits for heavy agent/model usage are:

- Project-wide rename, dead-code delete, new functions beside an old one are a single confined, previewed and revertible atomic call that costs a fraction of the tokens read-then-edit does with an extra layer of safety ([more info](#edits-go-through-ccc))

**ccc** stands on the shoulders of [Tree-Sitter](https://github.com/tree-sitter/tree-sitter). It scans a project and builds the **ccc** code map in memory.
This is a human and machine readable map of the source tree including every source file; its
Expand Down Expand Up @@ -149,8 +162,7 @@ ccc insights --html page.html # output format is html, as a single page

`ccc run --vis` serves a 2D map of the project at `http://127.0.0.1:6767/vis` and opens it in your
browser. It follows the C4 model from the whole system down to the code, then goes one level
further: the logic of a single function, drawn as a node graph in the style of Unreal Engine
Blueprints.
further: the logic of a single function, drawn as a node graph.

| level | boxes | arrows |
|---|---|---|
Expand All @@ -171,6 +183,23 @@ is a call the resolver found evidence for, and a branch is a node in the syntax
anything was seen to take. Calls that reach nothing in the project (the standard library, a
dependency) are folded into grey pills, and the toolbar can show them in full or hide them.

Along the foot of the page runs the timeline of what agents did through ccc - the same in a browser
as in the editor:

- **A definition at a time.** An edit is split into the definitions it touches - imports first,
then what it adds, then what it changes, then what it removes - so a replay builds the change up
one function, type or constant at a time, in the order the agent staged it.
- **Looks too.** `find`, `references`, `file` and `dependencies` calls land as looks at what they
named, so you can see what an agent read before it wrote anything.
- **At your pace.** Live keeps the time between steps as it was; ½×, ¼×, ⅛× and ⅒× stretch it, and
following runs behind by however many steps are still waiting.
- **One step on its own.** While a step is on show (◎), what it did not touch fades back, and a big
scene draws only what is wired to it, laid out together to fit the screen. Click the canvas to
bring the rest back.

Every ccc server on a project shares its steps through `.ccc/timeline.jsonl` (kept out of git), so
the visualiser shows an agent's work whichever server the agent talks to.

The data behind the page is plain JSON, with or without `--vis`:

```sh
Expand Down
283 changes: 278 additions & 5 deletions assets/vis/vis.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,18 @@ button, input { font: inherit; color: inherit; }

.brand {
display: flex;
align-items: baseline;
gap: 8px;
align-items: center;
white-space: nowrap;
}
.brand b { font-weight: 800; letter-spacing: -.01em; }
.brand span { color: var(--muted); font-size: 12px; }
/* the logo is white on clear, so it is drawn as a mask in the text's colour - it reads on either theme */
.brand .logo {
display: block;
width: 72px;
height: 16px;
background: var(--text);
-webkit-mask: url("data:image/png;base64,__CCC_LOGO__") center / contain no-repeat;
mask: url("data:image/png;base64,__CCC_LOGO__") center / contain no-repeat;
}

.crumbs {
display: flex;
Expand Down Expand Up @@ -253,6 +259,20 @@ button, input { font: inherit; color: inherit; }
touch-action: none;
}
.viewport.panning { cursor: grabbing; }
/* zoomed out this far the grid is noise, not a measure */
.viewport.far { background-image: none; }
/* the logo, faint and tiled, under everything drawn - fixed to the window,
so zooming and panning the canvas never move it */
.viewport::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: var(--text);
opacity: .01;
-webkit-mask: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='440' height='220'%3E%3Cimage href='data:image/png;base64,__CCC_LOGO__' x='30' y='40' width='110' height='25'/%3E%3Cimage href='data:image/png;base64,__CCC_LOGO__' x='250' y='150' width='110' height='25'/%3E%3C/svg%3E") 0 0 / 440px 220px repeat;
mask: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='440' height='220'%3E%3Cimage href='data:image/png;base64,__CCC_LOGO__' x='30' y='40' width='110' height='25'/%3E%3Cimage href='data:image/png;base64,__CCC_LOGO__' x='250' y='150' width='110' height='25'/%3E%3C/svg%3E") 0 0 / 440px 220px repeat;
}

.surface {
position: absolute;
Expand Down Expand Up @@ -639,9 +659,262 @@ button, input { font: inherit; color: inherit; }
}
.note-bar[hidden] { display: none; }

/* ---------- edit timeline ---------- */

/* what a step changed: added green, modified purple, removed red; a step not
written yet is dashed amber */
.node.chg {
--chg: var(--primary);
border-color: var(--chg);
box-shadow: var(--shadow), 0 0 0 3px color-mix(in srgb, var(--chg) 30%, transparent);
animation: chg-pulse 1.4s ease-out 2;
}
.node.chg-added { --chg: var(--green); }
.node.chg-removed { --chg: var(--red); opacity: .85; }
.node.chg-pending { --chg: var(--orange); border-style: dashed; }
/* what an agent looked at, rather than changed */
.node.chg-read { --chg: var(--blue); }
@keyframes chg-pulse {
0% { box-shadow: var(--shadow), 0 0 0 0 color-mix(in srgb, var(--chg) 60%, transparent); }
70% { box-shadow: var(--shadow), 0 0 0 16px color-mix(in srgb, var(--chg) 0%, transparent); }
100% { box-shadow: var(--shadow), 0 0 0 3px color-mix(in srgb, var(--chg) 30%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
.node.chg { animation: none; }
.viewport.glide .surface { transition: none !important; }
}
.viewport.glide .surface { transition: transform .6s cubic-bezier(.2, .7, .2, 1); }
.viewport.glide { transition: background-size .6s, background-position .6s; }
/* a step on show, seen alone: whatever it did not touch fades back */
.node.faded, .wire.faded, .elabel.faded, #frames.faded { opacity: .2; }

.timeline {
position: fixed;
left: 12px;
right: 12px;
bottom: 12px;
z-index: 14;
display: flex;
align-items: center;
gap: 8px;
padding: 5px 6px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow-bar);
}
.timeline[hidden] { display: none; }
/* no edits yet: only the switches for what comes stay, in the corner */
.timeline.empty { left: auto; }
.timeline.empty > :not(.tl-ws):not(.tl-follow):not(.tl-speed):not(.tl-focus) { display: none; }
body.with-tl-switches .zoom { bottom: 62px; }
body.with-timeline .legend { bottom: 62px; }
body.with-timeline .zoom { bottom: 62px; }
body.with-timeline .panel { bottom: 62px; }
.tl-controls { display: flex; gap: 2px; }
.timeline button {
height: 28px;
min-width: 28px;
padding: 0 8px;
border: 0;
border-radius: 4px;
background: none;
color: var(--tool);
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
.timeline button:hover:not(:disabled) { background: var(--hover); color: var(--tool-active); }
.timeline button:disabled { opacity: .4; cursor: default; }
.tl-track {
position: relative;
flex: 1 1 160px;
min-width: 80px;
height: 28px;
border-radius: 4px;
background: var(--surface-2);
cursor: pointer;
touch-action: none;
outline: none;
}
.tl-track:focus-visible { box-shadow: 0 0 0 2px var(--primary-soft); }
.tl-ticks { position: absolute; inset: 4px 4px; display: flex; align-items: flex-end; gap: 1px; pointer-events: none; }
.tl-ticks .tick { flex: 1 1 0; max-width: 14px; height: 55%; border-radius: 2px; background: var(--grey); opacity: .75; }
.tl-ticks .tick.staged { background: var(--orange); }
.tl-ticks .tick.applied { background: var(--green); }
.tl-ticks .tick.reverted { background: var(--red); }
.tl-ticks .tick.discarded { background: var(--grey); opacity: .45; }
.tl-ticks .tick.edited { background: var(--blue); }
.tl-ticks .tick.read { background: var(--primary); opacity: .4; height: 30%; }
.tl-ticks .tick.at { height: 100%; opacity: 1; box-shadow: 0 0 0 2px var(--primary); }
/* a change made by hand is a little person: the tick its body, this its head */
.tl-ticks .tick.human { position: relative; height: 40%; }
.tl-ticks .tick.human.at { height: 70%; }
.tl-ticks .tick.human::before {
content: '';
position: absolute;
left: 50%;
bottom: calc(100% + 1px);
width: 5px;
height: 5px;
border-radius: 50%;
background: inherit;
transform: translateX(-50%);
}
/* a step that only moved layout, while those are hidden */
.tl-ticks .tick.ws:not(.at) { height: 20%; opacity: .3; }
.timeline .tl-ws, .timeline .tl-speed, .timeline .tl-focus { border: 1px solid var(--border); border-radius: 14px; font-weight: 400; }
.timeline .tl-speed { min-width: 46px; font-variant-numeric: tabular-nums; }
.timeline .tl-ws[aria-pressed="true"], .timeline .tl-focus[aria-pressed="true"] { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); }
.panel .chg-tag.ws { background: var(--grey); }
.person, .glyph { flex: none; width: 12px; height: 12px; margin-right: 4px; vertical-align: -2px; fill: currentColor; }
.tl-step .person, .replay-badge .person, .tl-ask .person, .ask-row .person { color: var(--blue); }
.tl-ask .glyph, .ask-row .glyph { color: var(--primary); }
/* where one ask's steps end and the next one's begin */
.tl-ticks .tl-sep { flex: none; align-self: stretch; width: 2px; margin: 0 2px; border-radius: 1px; background: var(--border); }
.timeline .tl-ask {
flex: 0 1 auto;
min-width: 0;
max-width: 28ch;
overflow: hidden;
text-overflow: ellipsis;
border: 1px solid var(--border);
border-radius: 14px;
font-weight: 400;
font-size: 12px;
}
.timeline .tl-ask[hidden] { display: none; }
.panel .ask-session {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: 12px 0 4px;
color: var(--muted);
font-size: 11px;
text-transform: uppercase;
letter-spacing: .04em;
}
.panel .ask-session button { flex: none; white-space: nowrap; text-transform: none; letter-spacing: 0; }
.panel .ask-row { display: flex; align-items: stretch; gap: 2px; margin: 2px 0; border-radius: 6px; }
.panel .ask-row.at { background: var(--primary-soft); }
.panel .ask-go {
flex: 1 1 auto;
display: flex;
align-items: flex-start;
gap: 6px;
min-width: 0;
padding: 6px;
border: 0;
background: none;
color: var(--text);
text-align: left;
cursor: pointer;
}
.panel .ask-go:hover { background: var(--hover); }
.panel .ask-go .glyph, .panel .ask-go .person { margin-top: 2px; }
.panel .ask-text {
flex: 1 1 auto;
min-width: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.panel .ask-n { flex: none; color: var(--muted); font-size: 11px; }
.panel .ask-play { flex: none; border: 0; background: none; color: var(--tool); cursor: pointer; }
.panel .ask-play:hover { color: var(--primary); }
.timeline .tl-step {
flex: 0 1 auto;
min-width: 0;
max-width: 34ch;
overflow: hidden;
text-overflow: ellipsis;
font: 12px var(--mono);
font-weight: 400;
color: var(--muted);
}
.timeline .tl-step:disabled { opacity: 1; }
.tl-side { display: flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
.tl-side[hidden] { display: none; }
.timeline .tl-side button { border-radius: 0; font-weight: 400; font-size: 12px; }
.timeline .tl-side button[aria-pressed="true"] { background: var(--primary-soft); color: var(--primary); }
.timeline .tl-follow { border: 1px solid var(--border); border-radius: 14px; font-weight: 400; }
.timeline .tl-follow[aria-pressed="true"] { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); }

.replay-badge {
position: fixed;
left: 12px;
top: 64px;
z-index: 12;
padding: 4px 10px;
border: 1px solid var(--orange);
border-radius: 14px;
background: var(--surface);
color: var(--text);
font-size: 12px;
box-shadow: var(--shadow-bar);
max-width: calc(100vw - 24px);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.replay-badge[hidden] { display: none; }

.panel .chg-row { display: flex; align-items: baseline; gap: 8px; }
.panel .chg-tag {
flex: none;
padding: 0 6px;
border-radius: 8px;
font-size: 11px;
color: #fff;
background: var(--primary);
}
.panel .chg-tag.added { background: var(--green); }
.panel .chg-tag.removed { background: var(--red); }
.panel .chg-tag.read { background: var(--blue); }
.panel .diff-file { margin: 8px 0 2px; font: 12px var(--mono); }
.panel .diff-file span { color: var(--muted); }
.panel .diff pre {
margin: 0;
padding: 6px 8px;
max-height: 340px;
overflow: auto;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface-2);
font: 11px/1.45 var(--mono);
}
.panel .diff .add { display: block; background: color-mix(in srgb, var(--green) 16%, transparent); }
.panel .diff .del { display: block; background: color-mix(in srgb, var(--red) 14%, transparent); }
.panel .diff .hunk { display: block; color: var(--primary); }
.panel .diff .meta { display: block; color: var(--faint); }

@media (max-width: 900px) {
.brand span { display: none; }
.search input { width: 160px; }
.panel { width: auto; left: 12px; top: auto; height: 45vh; }
.panel:not([hidden]) ~ .zoom { right: 12px; bottom: calc(45vh + 24px); }
body.with-timeline .panel { bottom: 62px; }
body.with-timeline .panel:not([hidden]) ~ .zoom { bottom: calc(45vh + 74px); }
}

/* a VS Code side bar: one column - the crumbs get a row of their own above
search and the toggles, and the rest of the chrome folds away */
@media (max-width: 560px) {
.brand, .crumbs .lvl, .spacer, .legend, #layout-reset { display: none; }
.bar { height: 84px; flex-wrap: wrap; align-content: center; gap: 4px 6px; padding: 4px 8px; }
.crumbs { flex: 1 1 100%; order: -1; }
.crumbs button { max-width: 150px; padding: 3px 5px; }
.search { flex: 1 1 0; min-width: 0; }
.search input { width: 100%; height: 30px; }
.search svg { top: 8px; }
.tools .chip { padding: 0 6px; font-size: 11px; }
.viewport { top: 84px; }
.panel, .note-bar, .replay-badge { top: 94px; }
.timeline { flex-wrap: wrap; left: 6px; right: 6px; bottom: 6px; }
.tl-track { order: 5; flex-basis: 100%; }
.timeline .tl-step { flex: 1 1 0; max-width: none; }
.timeline .tl-ask { max-width: 12ch; }
body.with-timeline .zoom, body.with-timeline .panel { bottom: 88px; }
.panel { left: 6px; right: 6px; }
}
Loading
Loading