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
4 changes: 3 additions & 1 deletion .devia/01_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ src/cli.mjs argument parsing, command table, context (root, .devia, f
src/commands/*.mjs one file per command, each exporting a default (ctx, name) => exit code
src/lib/*.mjs yaml · fs · git · rules · ui · vendor · version — no command logic here
src/lib/*.mjs yaml · markdown · fs · git · rules · ui · vendor · version — no command logic
content rules/ · standard/ · checklists/ · templates/ (read, never imported)
```
Expand Down Expand Up @@ -52,6 +52,8 @@ Content is data. Code reads it; code never encodes what a rule says.
|---|---|---|
| No runtime dependencies | The tool that preaches `ARC-004` cannot carry a tree of its own | `.cursor/rules/devia.mdc`, `CONTRIBUTING.md` |
| Own YAML subset parser | Frontmatter and impact maps only; devia writes the files it reads | `src/lib/yaml.mjs` header |
| Own Markdown subset renderer | Same reason, same bar: the memory templates use a known subset, and `ARC-004` rules out a library. A line outside the subset is shown as written, never dropped | `src/lib/markdown.mjs` header |
| `devia read` embeds its content | A reader that needs a server is a reader nobody opens: the page works by double-click, offline | `src/commands/read.mjs` |
| Rule IDs are stable and never reused | Citations in old reports must keep resolving | `GOVERNANCE.md`, `rules/LIFECYCLE.md` |
| Pinning the standard into `.devia/standard/` is opt-in | 391 copied files against 17 of memory buries what the folder is for, and every sync becomes a 391-file diff. `devia rules` reads the standard; `devia sync` pins it for offline agents and audits | `src/commands/init.mjs`, `.devia/11_GAPS.md` G8 |
| The CLI version and the standard version move separately | A CLI fix must not force an adopter to re-pin the corpus | `src/lib/version.mjs` |
Expand Down
2 changes: 2 additions & 0 deletions .devia/02_SURFACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
| `devia check` | Readiness gates | `src/commands/check.mjs` | 1 on any P0 FAIL |
| `devia doctor` | Adoption, drift, staleness | `src/commands/doctor.mjs` | 1 when there is no `.devia/` |
| `devia rules` | Query the registry | `src/commands/rules.mjs` | 1 when `--id` is unknown |
| `devia read` | Render the memory as one self-contained page | `src/commands/read.mjs` | 1 without `.devia/` |
| `devia sync` | Pin the standard, or refresh a pinned copy | `src/commands/sync.mjs` | 1 without `.devia/` |
| `devia skills` | Install adapters and the skill pack, per repository or `--global` | `src/commands/skills.mjs` | 2 on a bad action |
| `devia gap` / `devia debt` | Registry lines | `src/commands/registry.mjs` | 1 when the id is unknown |
Expand All @@ -37,6 +38,7 @@ say where, or `--yes` to accept it. Nothing is written before that question is s
| `init` | `.devia/` (memory, `devia.json`, `impact-map.yaml`, `standard/`) |
| `init`, `skills install` | `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/devia.mdc`, `.github/copilot-instructions.md`, `.windsurfrules` |
| `skills install --skill` | `.cursor/skills/devia/SKILL.md`, `.claude/skills/devia/SKILL.md` |
| `read` | `.devia/reader.html` — a generated snapshot, gitignored, never the source |
| `skills install --global` | Outside the repository, in each agent's own configuration: `~/.claude/skills/devia/`, `~/.codex/skills/devia/`, `~/.cursor/rules/devia.mdc`, `~/.gemini/GEMINI.md` when empty. Copilot and Windsurf report `SKIP` (`12_DEBT.md` D8) |

`files` in `package.json` decides what npm ships. Adding a directory the CLI reads at runtime
Expand Down
1 change: 1 addition & 0 deletions .devia/11_GAPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ Add one with `npx devia gap add "question"`.
| G6 | Should devia install its contract at user level for every agent, or stay per-repository outside Claude Code? | devia is for every agent — user-level install built for Claude Code, Codex, Cursor and Gemini in 0.3.0 |
| G1 | Should `devia check` grow ecosystem-specific gates (Python, Go, Rust) or stay deliberately generic? | Reframed: the failure was not the ecosystem but the assumption that the manifest sits at the repository root — fixed in 0.4.0. Ecosystem-specific gates remain out of scope |
| G8 | Should devia pin a copy of the standard into every adopter repository by default? | No — opt-in via devia sync. Measured on a real repository: 391 pinned files against 17 of memory, tripling a 211-file project and turning every sync into a 391-file diff |
| G9 | Should devia render the memory itself, or leave reading to whatever the project already has? | devia renders it: `devia read` writes one self-contained page — no server, no network, no dependency (ARC-004) |
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ build/
.env.*
!.env.example

# Généré par `devia read` — un instantané de la mémoire, pas la mémoire
.devia/reader.html

# Local secrets / audits
*.pem
*.key
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## 0.6.0 — 2026-09-09

The standard is unchanged: `VERSION` stays at 0.1.0.

### `devia read` — the memory as one page

A memory nobody reads is documentation with extra steps. `devia read` renders `.devia/` into a
single HTML file: a sidebar in reading order — the contract first, then the numbered files — and
links between memory files that jump inside the page instead of asking the filesystem for them.

The page is **self-contained**: content embedded, no fetch, no CDN, no stylesheet to resolve. It
opens by double-click, offline. The reader that prompted this one needed a running local server,
because a page that fetches its own content hits CORS on `file://`.

- `devia read` writes `.devia/reader.html`; `--out` puts it elsewhere
- It is a **snapshot**, never the source: regenerate it after changing the memory. Adopters
should gitignore it, as this repository now does
- `src/lib/markdown.mjs` renders the subset the memory templates actually use — headings,
tables, fenced code, lists, quotes, rules, and a few inline marks. Same reasoning as the YAML
parser: devia writes the files it reads, and `ARC-004` rules out a library. Content is escaped
before anything else, so memory text cannot inject markup, and a line outside the subset is
shown as written rather than dropped

Recorded as G9.

### Fixed

- `devia --help` still described `sync` as refreshing a vendored standard, which 0.5.0 made
opt-in

## 0.5.0 — 2026-09-09

The standard is unchanged: `VERSION` stays at 0.1.0.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@schneiderjoseph/devia",
"version": "0.5.0",
"version": "0.6.0",
"description": "One standard, one memory: engineering and design rules plus living project memory for AI coding agents",
"type": "module",
"license": "MIT",
Expand Down
4 changes: 3 additions & 1 deletion src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const COMMANDS = {
check: () => import("./commands/check.mjs"),
doctor: () => import("./commands/doctor.mjs"),
rules: () => import("./commands/rules.mjs"),
read: () => import("./commands/read.mjs"),
sync: () => import("./commands/sync.mjs"),
skills: () => import("./commands/skills.mjs"),
gap: () => import("./commands/registry.mjs"),
Expand All @@ -24,7 +25,8 @@ ${color.bold("devia")} — one standard, one memory
${color.bold("devia check")} readiness gates — P0 failures exit non-zero
${color.bold("devia doctor")} adoption, drift and staleness diagnosis
${color.bold("devia rules")} list or show rules from the registry
${color.bold("devia sync")} refresh the vendored standard after upgrading devia
${color.bold("devia read")} render the memory as one self-contained page
${color.bold("devia sync")} pin the standard under .devia/standard/, or refresh it
${color.bold("devia skills")} install the agent adapters (install --agent all)
${color.bold("devia gap")} add or close a line in 11_GAPS.md
${color.bold("devia debt")} add or close a line in 12_DEBT.md
Expand Down
161 changes: 161 additions & 0 deletions src/commands/read.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import path from "node:path";
import { exists, read, writeFile, walk } from "../lib/fs.mjs";
import { renderMarkdown, escapeHtml } from "../lib/markdown.mjs";
import { cliVersion } from "../lib/version.mjs";
import { color, heading, status, line } from "../lib/ui.mjs";

/**
* The memory, as one page a human can read.
*
* Self-contained on purpose: the content is embedded, so the file opens by double-click with no
* server, no network and no CDN. A reader that needs a running server is a reader nobody opens.
*/

/** Reading order: the contract first, then the numbered files, then whatever else is there. */
function order(files) {
const rank = (f) => {
if (f === "AGENTS.md") return -2;
if (f === "README.md") return -1;
const n = f.match(/^(\d+)_/);
return n ? Number(n[1]) : 999;
};
return files.sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
}

function title(file) {
if (file === "AGENTS.md") return "AGENTS";
const body = file.replace(/\.md$/, "");
const m = body.match(/^(\d+)_(.*)$/);
return m ? `${m[1]} · ${m[2].replace(/_/g, " ")}` : body.replace(/_/g, " ");
}

const STYLE = `
:root{--ink:#121826;--ink-line:#222F45;--paper:#fff;--canvas:#EFEEEA;--line:#E7E7EA;
--muted:#6B7280;--muted-d:rgba(229,231,235,.62);--text-d:#E5E7EB;--accent:#FF5A5F;
--mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
*{box-sizing:border-box}
body{margin:0;background:var(--canvas);color:var(--ink);
font:15px/1.6 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;display:flex;min-height:100vh}
nav{width:290px;flex:0 0 290px;background:var(--ink);color:var(--text-d);position:sticky;top:0;
height:100vh;overflow:auto;padding:22px 16px}
nav h1{font-size:13px;letter-spacing:.28em;text-transform:uppercase;font-family:var(--mono);
color:var(--muted-d);margin:0 0 4px}
nav .sub{font-size:12px;color:var(--muted-d);margin-bottom:18px;line-height:1.4}
nav a{display:block;padding:7px 10px;border-radius:8px;color:var(--text-d);text-decoration:none;
font-size:13px;margin-bottom:2px}
nav a:hover{background:rgba(255,255,255,.06)}
nav a.active{background:rgba(255,90,95,.16);outline:1px solid rgba(255,90,95,.25)}
main{flex:1;min-width:0;padding:44px 56px;background:var(--paper)}
article{max-width:820px;display:none}
article.active{display:block}
h1,h2,h3,h4{line-height:1.25;margin:1.8em 0 .6em}
h1{font-size:30px;margin-top:0}h2{font-size:22px}h3{font-size:17px}h4{font-size:15px}
h2{border-top:1px solid var(--line);padding-top:1.2em}
p{margin:.7em 0}
a{color:var(--accent)}
code{font-family:var(--mono);font-size:.88em;background:var(--canvas);padding:2px 5px;border-radius:4px}
pre{background:var(--ink);color:var(--text-d);padding:16px 18px;border-radius:10px;overflow:auto}
pre code{background:none;color:inherit;padding:0;font-size:12.5px;line-height:1.55}
table{border-collapse:collapse;width:100%;margin:1.1em 0;font-size:14px;display:block;overflow-x:auto}
th,td{border:1px solid var(--line);padding:8px 11px;text-align:left;vertical-align:top}
th{background:var(--canvas);font-weight:600}
blockquote{margin:1.1em 0;padding:2px 0 2px 16px;border-left:3px solid var(--accent);color:var(--muted)}
hr{border:0;border-top:1px solid var(--line);margin:2em 0}
ul,ol{padding-left:1.3em}li{margin:.3em 0}
footer{margin-top:48px;padding-top:16px;border-top:1px solid var(--line);
font-family:var(--mono);font-size:11px;color:var(--muted)}
@media(max-width:820px){body{display:block}nav{width:auto;height:auto;position:static}main{padding:28px 20px}}
`;

const SCRIPT = `
var links = [].slice.call(document.querySelectorAll('nav a'));
var docs = [].slice.call(document.querySelectorAll('article'));
function show(id) {
docs.forEach(function (d) { d.classList.toggle('active', d.id === id); });
links.forEach(function (a) { a.classList.toggle('active', a.getAttribute('href') === '#' + id); });
if (location.hash !== '#' + id) history.replaceState(null, '', '#' + id);
window.scrollTo(0, 0);
}
links.forEach(function (a) {
a.addEventListener('click', function (e) { e.preventDefault(); show(a.getAttribute('href').slice(1)); });
});
// A link between memory files jumps inside the page instead of asking the filesystem for it.
[].slice.call(document.querySelectorAll('article a')).forEach(function (a) {
var href = a.getAttribute('href') || '';
if (!/^[^/:#]+\\.md(#.*)?$/.test(href)) return;
var id = 'doc-' + href.split('#')[0].replace(/\\.md$/, '');
if (!document.getElementById(id)) return;
a.addEventListener('click', function (e) { e.preventDefault(); show(id); });
});
show((location.hash || '').slice(1) || docs[0].id);
`;

export default async function readCommand(ctx) {
const { root, deviaDir, flags } = ctx;

if (flags.help) {
line(`
${color.bold("devia read")} — the memory as one self-contained page

--root <dir> repository to read
--out <file> where to write (default: .devia/reader.html)

The page embeds the memory: it opens by double-click, with no server and no network.
Regenerate it after changing the memory — it is a snapshot, never the source.
`.trim());
return 0;
}

if (!exists(deviaDir)) {
status("FAIL", "no .devia/", "run `devia init` first");
return 1;
}

const files = order(walk(deviaDir, { filter: (f) => f.endsWith(".md") && !f.includes(path.sep) }));
if (!files.length) {
status("FAIL", "no memory files to read", ".devia/ holds no markdown");
return 1;
}

const project = path.basename(root);
const nav = [];
const articles = [];
for (const file of files) {
const id = `doc-${file.replace(/\.md$/, "")}`;
nav.push(`<a href="#${id}">${escapeHtml(title(file))}</a>`);
articles.push(
`<article id="${id}">\n${renderMarkdown(read(path.join(deviaDir, file)) || "")}\n` +
`<footer>${escapeHtml(file)} · .devia/</footer></article>`
);
}

const today = new Date().toISOString().slice(0, 10);
const html = `<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${escapeHtml(project)} — memory</title>
<style>${STYLE}</style></head>
<body>
<nav><h1>${escapeHtml(project)}</h1>
<div class="sub">Project memory · read before the code, updated with it</div>
${nav.join("\n")}
</nav>
<main>
${articles.join("\n")}
</main>
<script>${SCRIPT}</script>
</body></html>
`;

const out = flags.out ? path.resolve(String(flags.out)) : path.join(deviaDir, "reader.html");
writeFile(out, html);

heading(`devia read — ${project}`);
status("PASS", `${files.length} memory files rendered`, `${Math.round(html.length / 1024)} kB`);
status("PASS", "written", out);
line("");
line(color.dim(" Open it directly — no server needed. Regenerate after changing the memory."));
line(color.dim(` Generated ${today} by devia ${cliVersion()}.`));
line("");
return 0;
}
Loading
Loading