@schneiderjoseph/devia 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/MIGRATION.md +3 -2
- package/README.md +21 -1
- package/package.json +1 -1
- package/skills/devia/SKILL.md +3 -2
- package/src/cli.mjs +3 -1
- package/src/commands/check.mjs +10 -3
- package/src/commands/doctor.mjs +4 -5
- package/src/commands/init.mjs +9 -6
- package/src/commands/read.mjs +161 -0
- package/src/lib/markdown.mjs +179 -0
- package/templates/agents/AGENTS.md +2 -2
- package/templates/agents/CLAUDE.md +2 -1
- package/templates/project/14_INDEX.md +12 -8
- package/templates/project/AGENTS.md +3 -3
- package/templates/project/README.md +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 — 2026-09-09
|
|
4
|
+
|
|
5
|
+
The standard is unchanged: `VERSION` stays at 0.1.0.
|
|
6
|
+
|
|
7
|
+
### `devia read` — the memory as one page
|
|
8
|
+
|
|
9
|
+
A memory nobody reads is documentation with extra steps. `devia read` renders `.devia/` into a
|
|
10
|
+
single HTML file: a sidebar in reading order — the contract first, then the numbered files — and
|
|
11
|
+
links between memory files that jump inside the page instead of asking the filesystem for them.
|
|
12
|
+
|
|
13
|
+
The page is **self-contained**: content embedded, no fetch, no CDN, no stylesheet to resolve. It
|
|
14
|
+
opens by double-click, offline. The reader that prompted this one needed a running local server,
|
|
15
|
+
because a page that fetches its own content hits CORS on `file://`.
|
|
16
|
+
|
|
17
|
+
- `devia read` writes `.devia/reader.html`; `--out` puts it elsewhere
|
|
18
|
+
- It is a **snapshot**, never the source: regenerate it after changing the memory. Adopters
|
|
19
|
+
should gitignore it, as this repository now does
|
|
20
|
+
- `src/lib/markdown.mjs` renders the subset the memory templates actually use — headings,
|
|
21
|
+
tables, fenced code, lists, quotes, rules, and a few inline marks. Same reasoning as the YAML
|
|
22
|
+
parser: devia writes the files it reads, and `ARC-004` rules out a library. Content is escaped
|
|
23
|
+
before anything else, so memory text cannot inject markup, and a line outside the subset is
|
|
24
|
+
shown as written rather than dropped
|
|
25
|
+
|
|
26
|
+
Recorded as G9.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `devia --help` still described `sync` as refreshing a vendored standard, which 0.5.0 made
|
|
31
|
+
opt-in
|
|
32
|
+
|
|
33
|
+
## 0.5.0 — 2026-09-09
|
|
34
|
+
|
|
35
|
+
The standard is unchanged: `VERSION` stays at 0.1.0.
|
|
36
|
+
|
|
37
|
+
### Pinning the standard is now opt-in
|
|
38
|
+
|
|
39
|
+
`devia init` used to copy the whole standard into `.devia/standard/`. Measured on a real
|
|
40
|
+
repository: **391 pinned files against 17 of memory** — a folder whose purpose is to be read by
|
|
41
|
+
a human and an agent, in which 96% of the files were a copy nobody wrote. On a 211-file project
|
|
42
|
+
it tripled the repository, and every `devia sync` produced a 391-file diff in which a real
|
|
43
|
+
memory change was invisible.
|
|
44
|
+
|
|
45
|
+
- `devia init` writes the memory and the adapters, and pins nothing: **17 files, 57 kB**
|
|
46
|
+
- `devia init --vendor` pins the copy for those who want it up front
|
|
47
|
+
- `devia sync` pins it on demand and refreshes it afterwards — that is now its first job, not
|
|
48
|
+
only its maintenance one
|
|
49
|
+
- `doctor` reports an unpinned standard as `INFO`, not a `WARN` to clear: the default is not a
|
|
50
|
+
defect
|
|
51
|
+
- The memory templates, the agent adapters and the skill read the rules with
|
|
52
|
+
`npx devia rules --id <ID>` / `--domain <name>` instead of linking into a copy that may not
|
|
53
|
+
exist. A pinned copy is mentioned as what it is: optional
|
|
54
|
+
|
|
55
|
+
Nothing to do when upgrading. An existing `.devia/standard/` is left alone, `devia sync` keeps
|
|
56
|
+
refreshing it, and only new `devia init` runs behave differently. Recorded as G8.
|
|
57
|
+
|
|
58
|
+
### A P0 blocker comes from the priority cell, never from prose
|
|
59
|
+
|
|
60
|
+
`MEM-DEBT-P0` matched `P0` anywhere in a debt row. A P1 line reading "becomes P0 once the
|
|
61
|
+
payment module ships" failed the gate, so a project with no P0 debt was told it was blocked by
|
|
62
|
+
one. The check now reads the priority cell. Found by writing a real project's debt registry.
|
|
63
|
+
|
|
3
64
|
## 0.4.0 — 2026-09-09
|
|
4
65
|
|
|
5
66
|
The standard is unchanged: `VERSION` stays at 0.1.0, no adopter needs `devia sync`.
|
package/MIGRATION.md
CHANGED
|
@@ -58,5 +58,6 @@ Then:
|
|
|
58
58
|
(undecided) and `.devia/12_DEBT.md` (decided, not built) — the split matters.
|
|
59
59
|
4. Replace `node scripts/production-check.mjs` in CI with `npx devia check`.
|
|
60
60
|
|
|
61
|
-
Nothing in the old repos is lost: the full text of both
|
|
62
|
-
|
|
61
|
+
Nothing in the old repos is lost: the full text of both ships with this package, readable with
|
|
62
|
+
`npx devia rules`, and `npx devia sync` pins a copy under `.devia/standard/` when a project
|
|
63
|
+
wants one on disk.
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ npx devia doctor # adoption + staleness diagnosis
|
|
|
56
56
|
├── 14_INDEX.md # where to find what
|
|
57
57
|
├── impact-map.yaml # change type → files that must be updated
|
|
58
58
|
├── devia.json # profile, modules, maturity target, pinned version
|
|
59
|
-
└── standard/ #
|
|
59
|
+
└── standard/ # optional: `devia sync` pins a copy of the standard here
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Plus adapters so every agent gets the same contract: `AGENTS.md` (universal), `CLAUDE.md`,
|
|
@@ -79,6 +79,26 @@ Update .devia/ in the SAME change
|
|
|
79
79
|
An agent that codes without reading the memory, or that ships code without updating it, has
|
|
80
80
|
failed the task — not styled it differently.
|
|
81
81
|
|
|
82
|
+
## What the gates actually caught
|
|
83
|
+
|
|
84
|
+
`PRINCIPLES.md` says evidence beats opinion, so here is the evidence. Every defect below was
|
|
85
|
+
found by devia's own gates, in one week, on devia itself and on one real project — a Next.js
|
|
86
|
+
application whose manifest lives in `apps/web/`. None of it is independent adoption; read it as
|
|
87
|
+
a tool being run in anger, not as a case study.
|
|
88
|
+
|
|
89
|
+
| Defect | Caught by |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Nine relative links resolved in this repository and not in the copy shipped to adopters — every project got nine broken links | The test that walks a materialised `.devia/` instead of listing it |
|
|
92
|
+
| `devia debt close` blanked a table row instead of removing it. A blank line ends a markdown table, so every debt line below the closed one was orphaned | Discharging a real debt line with the command itself |
|
|
93
|
+
| Two tests parsed `--json` from stdout merged with stderr. On a machine with `FORCE_COLOR` set, a Node warning made the JSON unparseable | `prepublishOnly`, which refused to publish |
|
|
94
|
+
| The skill told every agent to bootstrap with `npx devia init`. The package is scoped, so in a repository that has not installed devia that resolves to `404 devia@*` | Installing the skill system-wide, where a cold start is the normal case |
|
|
95
|
+
| Five gates reported `SKIP no package.json` to a repository that has one, with a lockfile, a lint script and thirteen dependencies. The letter of the rule held — nothing was rounded up to `PASS` — but the reason printed was false | Running `devia check` on a real project instead of a fixture |
|
|
96
|
+
| `MEM-DEBT-P0` matched `P0` anywhere in a debt row. A P1 line reading "becomes P0 once payments ship" reported a P0 blocker on a project that had none | Writing a real project's debt registry |
|
|
97
|
+
|
|
98
|
+
The last two are the ones worth dwelling on. A check that cannot answer must say so — but a
|
|
99
|
+
`SKIP` with a false reason, or a `FAIL` invented out of prose, is worse than no check at all,
|
|
100
|
+
because the reader believes the tool looked. Both are now regression tests.
|
|
101
|
+
|
|
82
102
|
## What is in the box
|
|
83
103
|
|
|
84
104
|
| Layer | Where | Content |
|
package/package.json
CHANGED
package/skills/devia/SKILL.md
CHANGED
|
@@ -40,8 +40,9 @@ the difference between a task and a guess (`AGT-002`).
|
|
|
40
40
|
|
|
41
41
|
## Step 2 — work under the rules
|
|
42
42
|
|
|
43
|
-
The rules
|
|
44
|
-
|
|
43
|
+
The rules have stable IDs and are read with `npx devia rules --id <ID>`, or `--domain <name>`
|
|
44
|
+
for a whole area. A project that ran `devia sync` also has them on disk under
|
|
45
|
+
`.devia/standard/rules/`. The ones that stop most bad changes:
|
|
45
46
|
|
|
46
47
|
| If you are about to… | Rule |
|
|
47
48
|
|---|---|
|
package/src/cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@ const COMMANDS = {
|
|
|
10
10
|
check: () => import("./commands/check.mjs"),
|
|
11
11
|
doctor: () => import("./commands/doctor.mjs"),
|
|
12
12
|
rules: () => import("./commands/rules.mjs"),
|
|
13
|
+
read: () => import("./commands/read.mjs"),
|
|
13
14
|
sync: () => import("./commands/sync.mjs"),
|
|
14
15
|
skills: () => import("./commands/skills.mjs"),
|
|
15
16
|
gap: () => import("./commands/registry.mjs"),
|
|
@@ -24,7 +25,8 @@ ${color.bold("devia")} — one standard, one memory
|
|
|
24
25
|
${color.bold("devia check")} readiness gates — P0 failures exit non-zero
|
|
25
26
|
${color.bold("devia doctor")} adoption, drift and staleness diagnosis
|
|
26
27
|
${color.bold("devia rules")} list or show rules from the registry
|
|
27
|
-
${color.bold("devia
|
|
28
|
+
${color.bold("devia read")} render the memory as one self-contained page
|
|
29
|
+
${color.bold("devia sync")} pin the standard under .devia/standard/, or refresh it
|
|
28
30
|
${color.bold("devia skills")} install the agent adapters (install --agent all)
|
|
29
31
|
${color.bold("devia gap")} add or close a line in 11_GAPS.md
|
|
30
32
|
${color.bold("devia debt")} add or close a line in 12_DEBT.md
|
package/src/commands/check.mjs
CHANGED
|
@@ -154,9 +154,16 @@ function makeChecks(root, ctx) {
|
|
|
154
154
|
run: () => {
|
|
155
155
|
const text = read(path.join(root, ".devia", "12_DEBT.md"));
|
|
156
156
|
if (text === null) return { kind: "SKIP", detail: "no debt registry" };
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
// The priority is a cell, not a word somewhere in the row. Matching the whole line made
|
|
158
|
+
// a P1 line reading "becomes P0 once the payment module ships" fail the gate — a P0
|
|
159
|
+
// blocker invented out of prose, on a project that had none.
|
|
160
|
+
const rows = (text.match(/^\|\s*D\d+\s*\|.*$/gm) || []).filter((row) => {
|
|
161
|
+
if (/TODO\(devia\)/.test(row)) return false;
|
|
162
|
+
return row
|
|
163
|
+
.split("|")
|
|
164
|
+
.slice(1, -1)
|
|
165
|
+
.some((cell) => cell.trim().toUpperCase() === "P0");
|
|
166
|
+
});
|
|
160
167
|
return rows.length
|
|
161
168
|
? { kind: "FAIL", detail: `${rows.length} P0 debt line(s) open` }
|
|
162
169
|
: { kind: "PASS" };
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -64,12 +64,11 @@ ${color.bold("devia doctor")} — adoption, drift and staleness
|
|
|
64
64
|
const vendored = walk(path.join(deviaDir, "standard")).length;
|
|
65
65
|
if (path.resolve(root) === path.resolve(packageRoot)) {
|
|
66
66
|
status("SKIP", "vendored standard", "this repository is the standard");
|
|
67
|
+
} else if (vendored) {
|
|
68
|
+
status("PASS", `standard pinned: ${vendored} files`);
|
|
67
69
|
} else {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`vendored standard: ${vendored} files`,
|
|
71
|
-
vendored ? "" : "run `devia sync`"
|
|
72
|
-
);
|
|
70
|
+
// Not pinning is the default, so absence is a fact to report, never a warning to clear.
|
|
71
|
+
status("INFO", "standard not pinned", "`devia rules` reads it; `devia sync` pins a copy");
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
// Adapters
|
package/src/commands/init.mjs
CHANGED
|
@@ -84,7 +84,7 @@ ${color.bold("devia init")} — create .devia/ in this repository
|
|
|
84
84
|
--profile <name> ${Object.keys(PROFILES).join(" | ")}
|
|
85
85
|
--force overwrite existing memory files (dangerous: they hold your decisions)
|
|
86
86
|
--no-agents do not write the agent adapters
|
|
87
|
-
--
|
|
87
|
+
--vendor pin a copy of the standard into .devia/standard/ (~390 files)
|
|
88
88
|
--yes accept a detected root that is not the current directory
|
|
89
89
|
`.trim());
|
|
90
90
|
return 0;
|
|
@@ -163,17 +163,20 @@ ${color.bold("devia init")} — create .devia/ in this repository
|
|
|
163
163
|
status("SKIP", "devia.json kept", "use --force to regenerate");
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
// 3.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
// 3. Pinned standard — opt-in. Vendoring writes ~390 files a project did not author, which
|
|
167
|
+
// buries the memory it is supposed to serve: on a real repository the ratio was 17 files of
|
|
168
|
+
// memory to 391 of copy, and every `sync` produced a 391-file diff. The rules stay reachable
|
|
169
|
+
// through `devia rules`, and `devia sync` pins the copy for whoever needs it offline.
|
|
170
|
+
if (flags.vendor && flags.vendor !== "false") {
|
|
170
171
|
const files = vendorStandard(path.join(deviaDir, "standard"), {
|
|
171
172
|
by: "devia init",
|
|
172
173
|
cli,
|
|
173
174
|
standard: version,
|
|
174
175
|
date: vars.DATE,
|
|
175
176
|
});
|
|
176
|
-
status("PASS", `standard
|
|
177
|
+
status("PASS", `standard pinned: ${files} files`, `v${version}`);
|
|
178
|
+
} else {
|
|
179
|
+
status("SKIP", "standard not pinned", "`devia sync` writes .devia/standard/ when you need it");
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
// 4. Agent adapters
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { exists, read, writeFile, walk } from "../lib/fs.mjs";
|
|
3
|
+
import { renderMarkdown, escapeHtml } from "../lib/markdown.mjs";
|
|
4
|
+
import { cliVersion } from "../lib/version.mjs";
|
|
5
|
+
import { color, heading, status, line } from "../lib/ui.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The memory, as one page a human can read.
|
|
9
|
+
*
|
|
10
|
+
* Self-contained on purpose: the content is embedded, so the file opens by double-click with no
|
|
11
|
+
* server, no network and no CDN. A reader that needs a running server is a reader nobody opens.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Reading order: the contract first, then the numbered files, then whatever else is there. */
|
|
15
|
+
function order(files) {
|
|
16
|
+
const rank = (f) => {
|
|
17
|
+
if (f === "AGENTS.md") return -2;
|
|
18
|
+
if (f === "README.md") return -1;
|
|
19
|
+
const n = f.match(/^(\d+)_/);
|
|
20
|
+
return n ? Number(n[1]) : 999;
|
|
21
|
+
};
|
|
22
|
+
return files.sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function title(file) {
|
|
26
|
+
if (file === "AGENTS.md") return "AGENTS";
|
|
27
|
+
const body = file.replace(/\.md$/, "");
|
|
28
|
+
const m = body.match(/^(\d+)_(.*)$/);
|
|
29
|
+
return m ? `${m[1]} · ${m[2].replace(/_/g, " ")}` : body.replace(/_/g, " ");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const STYLE = `
|
|
33
|
+
:root{--ink:#121826;--ink-line:#222F45;--paper:#fff;--canvas:#EFEEEA;--line:#E7E7EA;
|
|
34
|
+
--muted:#6B7280;--muted-d:rgba(229,231,235,.62);--text-d:#E5E7EB;--accent:#FF5A5F;
|
|
35
|
+
--mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
36
|
+
*{box-sizing:border-box}
|
|
37
|
+
body{margin:0;background:var(--canvas);color:var(--ink);
|
|
38
|
+
font:15px/1.6 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;display:flex;min-height:100vh}
|
|
39
|
+
nav{width:290px;flex:0 0 290px;background:var(--ink);color:var(--text-d);position:sticky;top:0;
|
|
40
|
+
height:100vh;overflow:auto;padding:22px 16px}
|
|
41
|
+
nav h1{font-size:13px;letter-spacing:.28em;text-transform:uppercase;font-family:var(--mono);
|
|
42
|
+
color:var(--muted-d);margin:0 0 4px}
|
|
43
|
+
nav .sub{font-size:12px;color:var(--muted-d);margin-bottom:18px;line-height:1.4}
|
|
44
|
+
nav a{display:block;padding:7px 10px;border-radius:8px;color:var(--text-d);text-decoration:none;
|
|
45
|
+
font-size:13px;margin-bottom:2px}
|
|
46
|
+
nav a:hover{background:rgba(255,255,255,.06)}
|
|
47
|
+
nav a.active{background:rgba(255,90,95,.16);outline:1px solid rgba(255,90,95,.25)}
|
|
48
|
+
main{flex:1;min-width:0;padding:44px 56px;background:var(--paper)}
|
|
49
|
+
article{max-width:820px;display:none}
|
|
50
|
+
article.active{display:block}
|
|
51
|
+
h1,h2,h3,h4{line-height:1.25;margin:1.8em 0 .6em}
|
|
52
|
+
h1{font-size:30px;margin-top:0}h2{font-size:22px}h3{font-size:17px}h4{font-size:15px}
|
|
53
|
+
h2{border-top:1px solid var(--line);padding-top:1.2em}
|
|
54
|
+
p{margin:.7em 0}
|
|
55
|
+
a{color:var(--accent)}
|
|
56
|
+
code{font-family:var(--mono);font-size:.88em;background:var(--canvas);padding:2px 5px;border-radius:4px}
|
|
57
|
+
pre{background:var(--ink);color:var(--text-d);padding:16px 18px;border-radius:10px;overflow:auto}
|
|
58
|
+
pre code{background:none;color:inherit;padding:0;font-size:12.5px;line-height:1.55}
|
|
59
|
+
table{border-collapse:collapse;width:100%;margin:1.1em 0;font-size:14px;display:block;overflow-x:auto}
|
|
60
|
+
th,td{border:1px solid var(--line);padding:8px 11px;text-align:left;vertical-align:top}
|
|
61
|
+
th{background:var(--canvas);font-weight:600}
|
|
62
|
+
blockquote{margin:1.1em 0;padding:2px 0 2px 16px;border-left:3px solid var(--accent);color:var(--muted)}
|
|
63
|
+
hr{border:0;border-top:1px solid var(--line);margin:2em 0}
|
|
64
|
+
ul,ol{padding-left:1.3em}li{margin:.3em 0}
|
|
65
|
+
footer{margin-top:48px;padding-top:16px;border-top:1px solid var(--line);
|
|
66
|
+
font-family:var(--mono);font-size:11px;color:var(--muted)}
|
|
67
|
+
@media(max-width:820px){body{display:block}nav{width:auto;height:auto;position:static}main{padding:28px 20px}}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const SCRIPT = `
|
|
71
|
+
var links = [].slice.call(document.querySelectorAll('nav a'));
|
|
72
|
+
var docs = [].slice.call(document.querySelectorAll('article'));
|
|
73
|
+
function show(id) {
|
|
74
|
+
docs.forEach(function (d) { d.classList.toggle('active', d.id === id); });
|
|
75
|
+
links.forEach(function (a) { a.classList.toggle('active', a.getAttribute('href') === '#' + id); });
|
|
76
|
+
if (location.hash !== '#' + id) history.replaceState(null, '', '#' + id);
|
|
77
|
+
window.scrollTo(0, 0);
|
|
78
|
+
}
|
|
79
|
+
links.forEach(function (a) {
|
|
80
|
+
a.addEventListener('click', function (e) { e.preventDefault(); show(a.getAttribute('href').slice(1)); });
|
|
81
|
+
});
|
|
82
|
+
// A link between memory files jumps inside the page instead of asking the filesystem for it.
|
|
83
|
+
[].slice.call(document.querySelectorAll('article a')).forEach(function (a) {
|
|
84
|
+
var href = a.getAttribute('href') || '';
|
|
85
|
+
if (!/^[^/:#]+\\.md(#.*)?$/.test(href)) return;
|
|
86
|
+
var id = 'doc-' + href.split('#')[0].replace(/\\.md$/, '');
|
|
87
|
+
if (!document.getElementById(id)) return;
|
|
88
|
+
a.addEventListener('click', function (e) { e.preventDefault(); show(id); });
|
|
89
|
+
});
|
|
90
|
+
show((location.hash || '').slice(1) || docs[0].id);
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
export default async function readCommand(ctx) {
|
|
94
|
+
const { root, deviaDir, flags } = ctx;
|
|
95
|
+
|
|
96
|
+
if (flags.help) {
|
|
97
|
+
line(`
|
|
98
|
+
${color.bold("devia read")} — the memory as one self-contained page
|
|
99
|
+
|
|
100
|
+
--root <dir> repository to read
|
|
101
|
+
--out <file> where to write (default: .devia/reader.html)
|
|
102
|
+
|
|
103
|
+
The page embeds the memory: it opens by double-click, with no server and no network.
|
|
104
|
+
Regenerate it after changing the memory — it is a snapshot, never the source.
|
|
105
|
+
`.trim());
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!exists(deviaDir)) {
|
|
110
|
+
status("FAIL", "no .devia/", "run `devia init` first");
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const files = order(walk(deviaDir, { filter: (f) => f.endsWith(".md") && !f.includes(path.sep) }));
|
|
115
|
+
if (!files.length) {
|
|
116
|
+
status("FAIL", "no memory files to read", ".devia/ holds no markdown");
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const project = path.basename(root);
|
|
121
|
+
const nav = [];
|
|
122
|
+
const articles = [];
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
const id = `doc-${file.replace(/\.md$/, "")}`;
|
|
125
|
+
nav.push(`<a href="#${id}">${escapeHtml(title(file))}</a>`);
|
|
126
|
+
articles.push(
|
|
127
|
+
`<article id="${id}">\n${renderMarkdown(read(path.join(deviaDir, file)) || "")}\n` +
|
|
128
|
+
`<footer>${escapeHtml(file)} · .devia/</footer></article>`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
133
|
+
const html = `<!doctype html>
|
|
134
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
135
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
136
|
+
<title>${escapeHtml(project)} — memory</title>
|
|
137
|
+
<style>${STYLE}</style></head>
|
|
138
|
+
<body>
|
|
139
|
+
<nav><h1>${escapeHtml(project)}</h1>
|
|
140
|
+
<div class="sub">Project memory · read before the code, updated with it</div>
|
|
141
|
+
${nav.join("\n")}
|
|
142
|
+
</nav>
|
|
143
|
+
<main>
|
|
144
|
+
${articles.join("\n")}
|
|
145
|
+
</main>
|
|
146
|
+
<script>${SCRIPT}</script>
|
|
147
|
+
</body></html>
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
const out = flags.out ? path.resolve(String(flags.out)) : path.join(deviaDir, "reader.html");
|
|
151
|
+
writeFile(out, html);
|
|
152
|
+
|
|
153
|
+
heading(`devia read — ${project}`);
|
|
154
|
+
status("PASS", `${files.length} memory files rendered`, `${Math.round(html.length / 1024)} kB`);
|
|
155
|
+
status("PASS", "written", out);
|
|
156
|
+
line("");
|
|
157
|
+
line(color.dim(" Open it directly — no server needed. Regenerate after changing the memory."));
|
|
158
|
+
line(color.dim(` Generated ${today} by devia ${cliVersion()}.`));
|
|
159
|
+
line("");
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Markdown subset, rendered to HTML.
|
|
3
|
+
*
|
|
4
|
+
* The same reasoning as `yaml.mjs`: devia writes the files it reads. The memory templates use
|
|
5
|
+
* headings, tables, fenced code, lists, blockquotes, rules and a few inline marks — so that is
|
|
6
|
+
* what this renders. Anything outside the subset survives as escaped text rather than being
|
|
7
|
+
* silently swallowed, because a reader that drops a line is worse than one that shows it plainly.
|
|
8
|
+
*
|
|
9
|
+
* A dependency would be the easy answer here, and `ARC-004` says it is not available.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const ESCAPES = [
|
|
13
|
+
["&", "&"],
|
|
14
|
+
["<", "<"],
|
|
15
|
+
[">", ">"],
|
|
16
|
+
['"', """],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function escapeHtml(text) {
|
|
20
|
+
let out = String(text);
|
|
21
|
+
for (const [from, to] of ESCAPES) out = out.split(from).join(to);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Inline marks, applied to already-escaped text. Code spans are lifted out first so their
|
|
27
|
+
* contents are never re-parsed. The placeholder is NUL-delimited on purpose: prose contains
|
|
28
|
+
* " 12 ", and a placeholder prose can produce turns a number into a code span.
|
|
29
|
+
*/
|
|
30
|
+
const MARK = "\u0000";
|
|
31
|
+
|
|
32
|
+
function inline(text) {
|
|
33
|
+
const spans = [];
|
|
34
|
+
let out = text.replace(/`([^`]+)`/g, (_, code) => {
|
|
35
|
+
spans.push(code);
|
|
36
|
+
return `${MARK}${spans.length - 1}${MARK}`;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, href) => {
|
|
40
|
+
const safe = /^(https?:|mailto:|#|[\w./-])/.test(href) ? href : "#";
|
|
41
|
+
return `<a href="${safe}">${label}</a>`;
|
|
42
|
+
});
|
|
43
|
+
out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
44
|
+
out = out.replace(/(^|[\s(])\*([^*\n]+)\*(?=[\s).,;:!?]|$)/g, "$1<em>$2</em>");
|
|
45
|
+
|
|
46
|
+
// Regex littérale avec des échappements \u0000 : aucun octet de contrôle dans la source.
|
|
47
|
+
return out.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code>${spans[Number(i)]}</code>`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isTableSeparator(line) {
|
|
51
|
+
return /^\|[\s:|-]+\|$/.test(line.trim()) && line.includes("-");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cells(line) {
|
|
55
|
+
return line
|
|
56
|
+
.trim()
|
|
57
|
+
.replace(/^\|/, "")
|
|
58
|
+
.replace(/\|$/, "")
|
|
59
|
+
.split("|")
|
|
60
|
+
.map((c) => inline(c.trim()));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Render a Markdown subset to an HTML fragment. */
|
|
64
|
+
export function renderMarkdown(source) {
|
|
65
|
+
const lines = escapeHtml(source).split(/\r?\n/);
|
|
66
|
+
const html = [];
|
|
67
|
+
let i = 0;
|
|
68
|
+
|
|
69
|
+
const flushList = (tag, items) => {
|
|
70
|
+
html.push(`<${tag}>`);
|
|
71
|
+
for (const item of items) html.push(`<li>${inline(item)}</li>`);
|
|
72
|
+
html.push(`</${tag}>`);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
while (i < lines.length) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
|
|
78
|
+
if (!line.trim()) {
|
|
79
|
+
i++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fenced code: kept verbatim, never re-parsed.
|
|
84
|
+
const fence = line.match(/^```(\w*)\s*$/);
|
|
85
|
+
if (fence) {
|
|
86
|
+
const body = [];
|
|
87
|
+
i++;
|
|
88
|
+
while (i < lines.length && !/^```\s*$/.test(lines[i])) body.push(lines[i++]);
|
|
89
|
+
i++;
|
|
90
|
+
const lang = fence[1] ? ` data-lang="${fence[1]}"` : "";
|
|
91
|
+
html.push(`<pre${lang}><code>${body.join("\n")}</code></pre>`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const heading = line.match(/^(#{1,4})\s+(.*)$/);
|
|
96
|
+
if (heading) {
|
|
97
|
+
const level = heading[1].length;
|
|
98
|
+
html.push(`<h${level}>${inline(heading[2].trim())}</h${level}>`);
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (/^(-{3,}|\*{3,})\s*$/.test(line.trim())) {
|
|
104
|
+
html.push("<hr>");
|
|
105
|
+
i++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Table: a header row followed by a separator row.
|
|
110
|
+
if (line.trim().startsWith("|") && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
|
|
111
|
+
const head = cells(line);
|
|
112
|
+
i += 2;
|
|
113
|
+
const body = [];
|
|
114
|
+
while (i < lines.length && lines[i].trim().startsWith("|")) body.push(cells(lines[i++]));
|
|
115
|
+
html.push("<table><thead><tr>");
|
|
116
|
+
for (const c of head) html.push(`<th>${c}</th>`);
|
|
117
|
+
html.push("</tr></thead><tbody>");
|
|
118
|
+
for (const row of body) {
|
|
119
|
+
html.push("<tr>");
|
|
120
|
+
for (const c of row) html.push(`<td>${c}</td>`);
|
|
121
|
+
html.push("</tr>");
|
|
122
|
+
}
|
|
123
|
+
html.push("</tbody></table>");
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Les lignes sont déjà échappées : le chevron d'une citation y est ">", pas ">".
|
|
128
|
+
if (/^>\s?/.test(line)) {
|
|
129
|
+
const quote = [];
|
|
130
|
+
while (i < lines.length && /^>\s?/.test(lines[i])) {
|
|
131
|
+
quote.push(lines[i++].replace(/^>\s?/, ""));
|
|
132
|
+
}
|
|
133
|
+
html.push(`<blockquote>${inline(quote.join(" "))}</blockquote>`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
138
|
+
const items = [];
|
|
139
|
+
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) {
|
|
140
|
+
let item = lines[i++].replace(/^\s*[-*]\s+/, "");
|
|
141
|
+
// A wrapped bullet continues on the next indented, non-marker line.
|
|
142
|
+
while (i < lines.length && /^\s{2,}\S/.test(lines[i]) && !/^\s*[-*]\s+/.test(lines[i])) {
|
|
143
|
+
item += " " + lines[i++].trim();
|
|
144
|
+
}
|
|
145
|
+
items.push(item);
|
|
146
|
+
}
|
|
147
|
+
flushList("ul", items);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (/^\s*\d+\.\s+/.test(line)) {
|
|
152
|
+
const items = [];
|
|
153
|
+
while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) {
|
|
154
|
+
let item = lines[i++].replace(/^\s*\d+\.\s+/, "");
|
|
155
|
+
while (i < lines.length && /^\s{2,}\S/.test(lines[i]) && !/^\s*\d+\.\s+/.test(lines[i])) {
|
|
156
|
+
item += " " + lines[i++].trim();
|
|
157
|
+
}
|
|
158
|
+
items.push(item);
|
|
159
|
+
}
|
|
160
|
+
flushList("ol", items);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Paragraph: consecutive plain lines, joined.
|
|
165
|
+
const para = [];
|
|
166
|
+
while (
|
|
167
|
+
i < lines.length &&
|
|
168
|
+
lines[i].trim() &&
|
|
169
|
+
!/^(#{1,4}\s|```|>|\s*[-*]\s|\s*\d+\.\s|\|)/.test(lines[i])
|
|
170
|
+
) {
|
|
171
|
+
para.push(lines[i++].trim());
|
|
172
|
+
}
|
|
173
|
+
if (para.length) html.push(`<p>${inline(para.join(" "))}</p>`);
|
|
174
|
+
// A line the subset does not recognise is shown as written, never dropped.
|
|
175
|
+
else html.push(`<p>${inline(lines[i++].trim())}</p>`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return html.join("\n");
|
|
179
|
+
}
|
|
@@ -31,5 +31,5 @@ npx devia check # readiness gates
|
|
|
31
31
|
Update `.devia/` in the same change (see `.devia/impact-map.yaml`), then report what you changed
|
|
32
32
|
**and what you did not verify**.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
Rules by ID: `npx devia rules --id SEC-001`, or by domain: `npx devia rules --domain database`.
|
|
35
|
+
A pinned copy lives under `.devia/standard/` only if this project ran `devia sync`.
|
|
@@ -31,4 +31,5 @@ npx devia check
|
|
|
31
31
|
|
|
32
32
|
Report the checks that ran, the rule IDs involved, and what you did **not** verify.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Rules by ID: `npx devia rules --id SEC-001`, or by domain: `npx devia rules --domain database`.
|
|
35
|
+
A pinned copy lives under `.devia/standard/` only if this project ran `devia sync`.
|
|
@@ -38,13 +38,17 @@
|
|
|
38
38
|
|---|---|---|
|
|
39
39
|
| TODO(devia) | | |
|
|
40
40
|
|
|
41
|
-
## Standard
|
|
41
|
+
## Standard
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
The standard is read through the CLI, not copied into this repository.
|
|
44
|
+
|
|
45
|
+
| Need | Command |
|
|
44
46
|
|---|---|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
| A rule by ID | `npx devia rules --id SEC-001` |
|
|
48
|
+
| Every rule in a domain | `npx devia rules --domain database --priority P0` |
|
|
49
|
+
| Work contract | `AGENTS.md` at this repository's root |
|
|
50
|
+
| Readiness gates | `npx devia check` |
|
|
51
|
+
|
|
52
|
+
`npx devia sync` pins a version-locked copy under `standard/` when an agent must read it
|
|
53
|
+
offline, or when an audit needs the exact wording you built against. Add the paths here if you
|
|
54
|
+
do.
|
|
@@ -8,8 +8,8 @@ This file is your contract. Violating it is a failed task.
|
|
|
8
8
|
1. Read [`10_NEVER_ALWAYS.md`](10_NEVER_ALWAYS.md) — what this project has already banned
|
|
9
9
|
2. Read [`00_OVERVIEW.md`](00_OVERVIEW.md) — what this project is
|
|
10
10
|
3. Read the memory file for the surface you are about to change ([`14_INDEX.md`](14_INDEX.md))
|
|
11
|
-
4. Read the
|
|
12
|
-
|
|
11
|
+
4. Read the rules for what you are touching: `npx devia rules --domain <name>`, or
|
|
12
|
+
`npx devia rules --id <ID>` for one
|
|
13
13
|
|
|
14
14
|
Then work. Then update this memory in the same change.
|
|
15
15
|
|
|
@@ -48,4 +48,4 @@ TODO(devia): add this project's own commands — install, dev, test, lint, migra
|
|
|
48
48
|
- Not verified: ...
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Full contract, routing table and hard stops:
|
|
51
|
+
Full contract, routing table and hard stops: the `AGENTS.md` at this repository's root.
|
|
@@ -27,8 +27,11 @@ Created by `devia init` (devia {{DEVIA_VERSION}}, {{DATE}}).
|
|
|
27
27
|
Machine files: [`devia.json`](devia.json) (profile, maturity, pinned version) and
|
|
28
28
|
[`impact-map.yaml`](impact-map.yaml) (change type → files to update).
|
|
29
29
|
|
|
30
|
-
The standard itself is
|
|
31
|
-
|
|
30
|
+
The standard itself is not copied in here. Read it with `npx devia rules --id SEC-001` or
|
|
31
|
+
`npx devia rules --domain database`, which is the same text for every agent. If you need it on
|
|
32
|
+
disk — an agent with no network, or an audit that must show the exact wording you built against
|
|
33
|
+
— `npx devia sync` pins a version-locked copy under `standard/`, and `14_INDEX.md` then points
|
|
34
|
+
at it.
|
|
32
35
|
|
|
33
36
|
## The two registries
|
|
34
37
|
|
|
@@ -43,7 +46,7 @@ for every agent, offline, no network and no package manager required.
|
|
|
43
46
|
npx devia validate # structure, registries, placeholders
|
|
44
47
|
npx devia doctor # is the memory older than the code?
|
|
45
48
|
npx devia check # readiness gates
|
|
46
|
-
npx devia sync #
|
|
49
|
+
npx devia sync # pin the standard under standard/, or refresh a pinned copy
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
`.devia/` is committed. It is part of the repository, not a local scratch pad.
|