create-evda 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mike Veson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # @evda/person-record
2
+
3
+ The person layer over the substrate. An EVÐA is a directory that knows what it is: a person's record in their own words, a ledger of claims about them and what happened to each claim, and the operating instructions beside them, so that any agent on any model can open it and act correctly with no outside context.
4
+
5
+ This package scaffolds one and ships the optional tooling. The files are the truth. Nothing here is required to read or write a record; `evda doctor` says whether one is well-formed and `evda reindex` rebuilds the derived directory, and both can be thrown away. It has no runtime dependencies (`test/deps.test.mjs` asserts it) and never makes a network call.
6
+
7
+ Design contract: Spec 04 (the hypothesis ledger), Spec 05 (the data layer) and Spec 07 (holders, circles, references and views) in the EVÐA project. A short copy of what this package settles is in `docs/canon/person-record.md`.
8
+
9
+ ## Where it sits
10
+
11
+ Block tier, domain neutral. It imports nothing from the workspace at runtime. What it shares with the substrate is a file format: a circle is an eco, declared by the same `eco.yaml` that `@evda/common` parses and `@evda/vault-core` stores, and `test/eco-parity.test.mjs` runs every manifest this package writes through the substrate's own parser. Grants, entity addressing, changes and sync stay the substrate's; this package reads and writes plain files and leaves those to it.
12
+
13
+ ## Scaffold
14
+
15
+ ```bash
16
+ node create-evda.mjs my-evda --owner "Dana"
17
+ ```
18
+
19
+ Writes:
20
+
21
+ my-evda/
22
+ evda.json schema version, layers, and the MAPPING of roles to paths
23
+ AGENTS.md the operating layer. One installer-owned fence; the rest is the owner's
24
+ CLAUDE.md one line: @AGENTS.md (Claude Code does not read AGENTS.md on its own)
25
+ record/daily/ YYYY-MM-DD.md, verbatim. Day one holds one file: today's, recording
26
+ that the record was created. Everything after it is the owner's.
27
+ record/captures/ each record folder carries folder.evda.json: what its entries are
28
+ record/people/ (record, capture, person), which connectors feed its references,
29
+ and which views render it
30
+ ledger/hypotheses.md the entry shape and the six rules are at the top of the file
31
+ circles/, grants/ with --layers record,ledger,circles: each circle's eco.yaml, and one
32
+ grant file per reader and circle
33
+ .evda/version the schema version actually on disk
34
+ .evda/due.jsonl derived from the ledger's Due lines
35
+ .evda/digest.md derived from the fenced block of AGENTS.md, under 2048 bytes
36
+
37
+ Then open the directory in Claude Code, or any agent that reads AGENTS.md, and say something true about today. It goes into `record/daily/` in your words. Nothing is proposed about you until the record holds something to be wrong about.
38
+
39
+ The tree above is what the scaffolder creates and not a requirement. `evda.json` carries a `mapping` from roles to paths, and every tool reads roles. An existing folder of notes becomes an EVÐA by adding `evda.json` with a mapping that names where things already are, and nothing moves.
40
+
41
+ ## The CLI
42
+
43
+ evda doctor [dir] validate, and say what was examined
44
+ evda reindex [dir] [--adopt] rebuild .evda/
45
+ evda due [dir] what is due, soonest first
46
+ evda chain <H-id> [dir] the supersession chain through one hypothesis
47
+ evda current [dir] the derived "current" view of the ledger
48
+ evda moment <from> [to] [dir] what was written between two dates
49
+ evda upgrade [dir] replace the installer-owned block of AGENTS.md, touch nothing else
50
+ evda digest [dir] the instruction-only digest of AGENTS.md
51
+ evda migrate [dir] forward migrations, each saying what it cannot reverse
52
+ evda vector <vdir> [--check] the conformance projection of one ledger
53
+ evda references [dir] [--resolve]
54
+ every reference, its folder's connector, and what each source answers
55
+ evda folder <dir> --type T [--connectors a,b]
56
+ write a folder's folder.evda.json
57
+ evda grant <circle> --to <record-id> --kind person|agent|company [--name N]
58
+ let another record read a circle; an agent gets viewer only
59
+ evda grants [dir] every grant, who holds it and on what
60
+ evda revoke <circle> --to <record-id>
61
+ evda access <circle> --as <record-id>
62
+ may that record read the circle? exit 0 or 2, with why
63
+ evda status [dir] one screen
64
+ evda where which record the locator resolves to, and by which rule
65
+ evda use <dir> point ~/.evda/current at a record
66
+
67
+ The record is found through the locator: a directory argument, then `$EVDA_HOME`, then the nearest ancestor of the working directory holding `evda.json`, then `~/.evda/current`. Nothing assumes it was launched inside one.
68
+
69
+ `evda doctor` prints `CHECKED <n> <noun>` for everything it examined, or `SKIPPED <noun>: why`. `CHECKED 0` is a failure. A run that exits 0 without a `CHECKED` line of `n > 0` is itself a failure. Exit codes: 0 ok, 1 the record has a named problem, 2 refused with nothing written.
70
+
71
+ ## Circles are ecos
72
+
73
+ A circle is a subdirectory of the `circles` role holding `eco.yaml`, the substrate's eco manifest. The manifest declares it; a directory without one is not a circle and `evda doctor` fails on it.
74
+
75
+ ```yaml
76
+ id: ecos/public
77
+ kind: eco
78
+ schemaVersion: "0.1"
79
+ namespace: public.eco
80
+ title: Public
81
+ description: Anyone may read this circle. It holds what Dana publishes on purpose.
82
+ public: true
83
+ maintainers:
84
+ - name: Dana
85
+ role: holder
86
+ members:
87
+ writing:
88
+ path: record/writing
89
+ posts:
90
+ table: posts
91
+ where:
92
+ published: true
93
+ policy:
94
+ - rule: only what is published, or approved to publish, is in public
95
+ since: 2026-09-16
96
+ from: '"if it is not in the list it is not public, full stop" (Dana, record/daily/2026-09-16.md)'
97
+ passes: a post under record/writing
98
+ fails: a daily file that mentions the post
99
+ check: every file the circle covers sits under a path this manifest names; anything else fails
100
+ status: live
101
+ ```
102
+
103
+ The first six keys are the substrate's, and `id` and `namespace` follow from the directory name so the three cannot disagree. `maintainers` are the holders. `members` is the mapping principle one level down: each member names where the circle's bytes already live, relative to the record root, and nothing moves. A `path` member must exist on disk. A `table` member names rows on a host selected by equality, and is allowed only in a public circle, because a private circle's host may not read it. `public: true` is the whole of what makes a circle public; the reader takes the same path and needs no key. Reading an encrypted circle is not built; a private circle is validated for shape and never read.
104
+
105
+ `policy` is a list of rules, and a rule behaves like a test: one line in the owner's words, the date, the correction that made it (verbatim, quoted), an example that passes, one that fails, a check written so it could come back false, and a status of `live`, `revised` or `retired`. A rule is never edited in place; a revision is a new rule with `revisedBy` written on the old one. A number in a rule line is warned about: the check may count, the rule may not.
106
+
107
+ Every entry resolves to exactly one circle. `evda doctor` counts the record's entries (every file in a record role, plus the ledger) against the path members of every circle, prints `CHECKED <n> entries across <m> circles`, and fails on an entry no circle covers or two circles cover.
108
+
109
+ `circle.json`, the manifest this package wrote before circles became ecos, is still read with a warning so a record written against it keeps working. Nothing writes it any more.
110
+
111
+ `lib/circles.mjs` is the reader and writer: `readCircles`, `publicCircle`, `member`, `holders`, `coverage`, `circleManifest`, `rule`, `writeCircle`. `lib/yaml.mjs` is the YAML subset it reads and writes, checked against the `yaml` package over the substrate's own manifests in `test/yaml.test.mjs`.
112
+
113
+ ## Holders and grants
114
+
115
+ A record is held by a person, an agent, or a company (Spec 07, section 1). `evda.json` says which: `owner.kind`, a person unless said otherwise, and `id`, the record's atlas id (`<name>.atlas` unless given), which is how another record names this one in a grant or a reference. `create-evda my-agent --kind agent --id iris.atlas` scaffolds an agent's record; it has the same shape as a person's.
116
+
117
+ Who may read a circle is a set of grants, one file each under the `grants` role, in the substrate's own Member shape (`SCHEMA-member`), at `grants/peers/<record-id>--<circle>.eco.yaml`:
118
+
119
+ ```yaml
120
+ id: members/peers/iris.atlas--workshop.eco
121
+ kind: member
122
+ schemaVersion: "0.1"
123
+ principalKind: peer
124
+ principalId: iris.atlas
125
+ eco: workshop.eco
126
+ role: viewer
127
+ grantedAt: 2026-09-16T15:00:00.000Z
128
+ grantedBy: dana.atlas
129
+ holder:
130
+ kind: agent
131
+ name: Iris
132
+ ```
133
+
134
+ Every grantee is a `peer` (Spec 00 #65): a holder owns a record, a record is an atlas, and `peer` is the substrate's principal kind for another atlas. `holder` is the person-layer key the substrate's parser preserves, so a circle can say what kind of holder reads it without opening the peer's record. An agent holds `viewer` and nothing else; a grant giving it more is refused. A public circle needs no grant and a grant on one is refused. Grants live outside the circles they govern, as the substrate keeps `members/` outside `ecos/`: the access answer cannot sit inside the bytes it protects. `test/grants.test.mjs` hands every grant this package writes to `parseMember` and `Member.fromJSON` from `@evda/common`.
135
+
136
+ evda grant <circle> --to <record-id> --kind agent --name Iris viewer, the only role an agent gets
137
+ evda access <circle> --as <record-id> exit 0 allowed, 2 refused, with why
138
+ evda grants every grant, who holds it and on what
139
+ evda revoke <circle> --to <record-id>
140
+
141
+ `access` never answers with a bare yes or no: a pass names the grant that allowed it, a refusal names what is missing. `readCircleAs(evda, circle, id)` in `lib/grants.mjs` is the same gate for code, and throws `REFUSED` with exit 2 on a circle the reader was not granted. `evda doctor` prints `CHECKED <n> grants across <m> circle(s)` and fails a grant on a circle that does not exist, one that gives an agent more than viewer, and one whose filename disagrees with its content. Reading an encrypted circle's bytes is still not built; the gate decides, and what is behind it is declared by path.
142
+
143
+ ## References instead of copies
144
+
145
+ A reference is a stable pointer to something the record does not hold: a Contacts entry, a password manager item, a page, an entry in another EVÐA. It is written once, in the entry's frontmatter, and resolved when read. What is stored is the pointer, a label to render when the source is unreachable, and the date it last resolved. Never the payload, and never a secret.
146
+
147
+ ```yaml
148
+ ---
149
+ references:
150
+ - ref: contacts:2F1A-77
151
+ label: Sam Rivera
152
+ resolved: 2026-09-10
153
+ - ref: 1password:op://Personal/Bank
154
+ label: Bank login
155
+ - ref: url:https://example.org/paper
156
+ label: The paper
157
+ - ref: "@north.atlas/people.eco/person/sam@example.org"
158
+ label: Sam, in their own record
159
+ ---
160
+ ```
161
+
162
+ Four schemes: `contacts:<identifier>`, `1password:op://<vault>/<item>` (the item, never a field), `url:<http(s)://...>`, and `@<atlasId>/<eco>/<kind>/<id>`, the substrate's federated `EntityRef` unchanged (`test/entity-ref-parity.test.mjs` runs this package's parser against `parseEntityRef` from `@evda/common`). A key beyond `ref`, `label` and `resolved` is a copy of what the source holds and fails. A key named like a secret, or a value shaped like one (a private key block, a JWT, the prefixes the common services stamp on tokens, a bearer header, a password or credential in a URL, a long high-entropy token), is `REFUSED` with exit 2 and the value is never echoed.
163
+
164
+ Each record folder carries `folder.evda.json`:
165
+
166
+ ```json
167
+ {
168
+ "type": "person",
169
+ "connectors": ["contacts", "url"],
170
+ "views": [{ "view": "everyone", "of": "person", "in": "people.eco", "path": "record/people/Everyone.md" }]
171
+ }
172
+ ```
173
+
174
+ `type` is what the entries are (one of the Spec 07 types). `connectors` are the schemes the folder's references may resolve through; the registry reads this list first and skips a scheme the folder does not name. `views` are the folder's view declarations (Spec 07, section 5): each is the object `@evda/views` renders from, with `view` (its name), `of` (the entity kind), `in` (the eco), `path` (the markdown file it renders to, relative to the record root), and optionally `status` and `group`. This package checks the shape, that the file at `path` exists, and that its header names the same view; what the header means is the renderer's to refuse. The manifest never names a circle. A file inherits the nearest manifest above it. `evda doctor` prints `CHECKED <n> folder manifests`, `CHECKED <n> views` and `CHECKED <n> references across <m> file(s)`, and fails on a declared view with no file or a file whose header names another view, and on a reference with no manifest above it, one whose scheme the folder does not declare, one carrying a copy, and one carrying a secret. Doctor never touches the network.
175
+
176
+ `lib/resolvers.mjs` is the registry, one resolver per scheme. Resolving returns `resolved` (with the date and the source's own name for the thing, offered as a label and nothing more), `unreachable` (asked, no answer, with why), or `skipped` (not asked, with why). Never nothing, and never the payload. The `url` resolver is Node's `fetch` with a timeout and a capped read for the page's title; `test/resolvers.test.mjs` runs it against a local server. `contacts`, `1password` and the EVÐA address say `SKIPPED` with the reason, because this tool has no connector to the device's Contacts, to the password manager, or to another record's grants. A reference renders as its label, and an unresolved one says so and since when; `renderReference` never returns an empty string. `evda references --resolve` is the command that asks.
177
+
178
+ ## Versioning
179
+
180
+ `evda.json` declares a schema version. `.evda/version` records what a tool last left on disk. A disagreement refuses loudly rather than guessing. Migrations live in `migrations/`, forward-only, one version step per file, each required to say what it cannot reverse.
181
+
182
+ A tool reading a manifest newer than itself operates the layers it knows and writes back every key it did not understand, in the order it read them. `test/manifest.test.mjs` is the proof.
183
+
184
+ ## Generated files
185
+
186
+ `templates/AGENTS.md`, `templates/evda-digest.md`, `templates/hypotheses.md` and `vectors/007-empty-with-header/ledger.md` are generated from the spec (`AGENTS-template.md`, `04 - The Hypothesis Ledger.md`) by `scripts/build-templates.mjs`. Do not edit them; edit the source and rebuild. The spec is owned outside this repository; `EVDA_SPEC_DIR` names its directory. `pnpm templates:check` fails on drift when it is set and prints `SKIPPED` when it is not.
187
+
188
+ ## What stays outside the repository
189
+
190
+ Nothing in this package belongs to one person's record. Two things that do are reached by environment variable and are `SKIPPED` when unset, which is the normal state on every machine but the owner's:
191
+
192
+ - `EVDA_LEDGER_PY`: the second reader of the ledger format, in Python. `scripts/vectors-both.sh` runs both readers over every vector and compares when it is set.
193
+ - `EVDA_EXTRA_VECTORS`: a directory of conformance vectors cut from a real record. `test/vectors.test.mjs` and `vectors-both.sh` read it when set.
194
+
195
+ ## Checks
196
+
197
+ ```bash
198
+ pnpm --filter @evda/person-record test # node --test, 101 tests
199
+ pnpm --filter @evda/person-record lint # no em or en dashes; generated templates match the spec when reachable
200
+ pnpm --filter @evda/person-record vectors:both # both readers over every vector; SKIPPED for the one not reachable
201
+ ```
202
+
203
+ The package tests need `@evda/common` built first (`pnpm --filter @evda/common... build`), for the one test that hands this package's manifests to the substrate's parser. `pnpm test` at the root does that ordering.
204
+
205
+ ## Publishing
206
+
207
+ Not published. The bins are `create-evda` and `evda`; a public package under the `create-evda` name so that `npx create-evda my-evda` works is the owner's decision and its own ticket.
package/bin/evda.mjs ADDED
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ // evda: operate an EVDA from the command line. Optional. The files are the contract.
3
+ //
4
+ // Every command finds the record through the locator (lib/locate.mjs): an explicit directory,
5
+ // then $EVDA_HOME, then the nearest ancestor of the working directory holding evda.json, then
6
+ // ~/.evda/current. Nothing assumes it was launched inside one.
7
+
8
+ import { resolve, join, relative } from "node:path";
9
+ import { readFileSync, writeFileSync, statSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import { openEvda, readManifest, readVersion, writeVersion, findRoot, SCHEMA_VERSION } from "../lib/manifest.mjs";
12
+ import { locate, setPointer, pointerPath } from "../lib/locate.mjs";
13
+ import { resolveAll, agentsFile } from "../lib/roles.mjs";
14
+ import { doctor } from "../lib/doctor.mjs";
15
+ import { reindex, currentIndex, expectedIndex, expectedDigest } from "../lib/index.mjs";
16
+ import { readLedger, chain, current } from "../lib/ledger.mjs";
17
+ import { moment } from "../lib/record.mjs";
18
+ import { migrate } from "../lib/migrate.mjs";
19
+ import { splitFence, replaceFence, shippedGenerated } from "../lib/agents.mjs";
20
+ import { mdFiles } from "../lib/record.mjs";
21
+ import { folderManifests, folderFor, writeFolderManifest, FOLDER_MANIFEST } from "../lib/folders.mjs";
22
+ import { referencesIn, renderReference } from "../lib/references.mjs";
23
+ import { resolveReference } from "../lib/resolvers.mjs";
24
+ import { grant, revoke, readGrants, access } from "../lib/grants.mjs";
25
+ import { holderOf, HOLDER_KINDS } from "../lib/manifest.mjs";
26
+ import { EvdaError, isEntry } from "../lib/errors.mjs";
27
+
28
+ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
29
+
30
+ evda doctor [dir] validate, and say what was examined (CHECKED n ...)
31
+ evda reindex [dir] [--adopt] rebuild .evda/ (due.jsonl, digest.md) from the files
32
+ evda due [dir] what is due, soonest first
33
+ evda chain <H-id> [dir] the supersession chain through one hypothesis
34
+ evda current [dir] the derived "current" view of the ledger
35
+ evda moment <from> [to] [dir] what was written between two dates
36
+ evda upgrade [dir] replace the installer-owned block of the operating layer; touch nothing else
37
+ evda digest [dir] print the instruction-only digest of the operating layer
38
+ evda migrate [dir] forward migrations, each saying what it cannot reverse
39
+ evda vector <vdir> [--check] the conformance projection of <vdir>/ledger.md (Spec 04 vectors)
40
+ evda references [dir] [--resolve] [--timeout ms]
41
+ every reference in the record, its folder's connector, and with
42
+ --resolve what each source answers (url fetches; the rest say SKIPPED)
43
+ evda folder <dir> --type T [--connectors a,b] [--force]
44
+ write <dir>/folder.evda.json: what the folder holds and what feeds it
45
+ evda grant <circle> --to <record-id> --kind person|agent|company [--name N] [--role viewer] [dir]
46
+ let another record read a circle. An agent gets viewer only
47
+ evda grants [dir] every grant, who holds it and on what
48
+ evda revoke <circle> --to <record-id> [dir]
49
+ evda access <circle> --as <record-id> [dir]
50
+ may that record read the circle? exit 0 yes, 2 refused, with why
51
+ evda status [dir] one screen summary
52
+ evda where which EVDA the locator resolves to, and by which rule
53
+ evda use <dir> point ~/.evda/current at a record
54
+ evda init <dir> [--name N] [--owner O] [--layers record,ledger]
55
+
56
+ Locator order: directory argument, $EVDA_HOME, the working directory's ancestors, ~/.evda/current.
57
+ Exit codes: 0 ok, 1 the record has a problem and it was named, 2 refused (nothing written).`;
58
+
59
+ // A positional argument is the record when it looks like a path or IS a directory on disk. A bare
60
+ // name that exists (`evda doctor my-evda`) must not fall through to the pointer: on 2026-09-16 it
61
+ // did, and `evda doctor scratch` read the record ~/.evda/current pointed at instead of `scratch/`.
62
+ function isDirArg(a) {
63
+ if (a.includes("/") || a === "." || a === "..") return true;
64
+ try { return statSync(a).isDirectory(); } catch { return false; }
65
+ }
66
+
67
+ // `--name value` or `--name=value`. The value of a `--name value` pair is not a positional.
68
+ const VALUE_FLAGS = ["to", "kind", "name", "role", "by", "as"];
69
+ function flagValue(args, name) {
70
+ const eq = args.find((a) => a.startsWith(`--${name}=`));
71
+ if (eq) return eq.slice(name.length + 3);
72
+ const i = args.indexOf(`--${name}`);
73
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
74
+ }
75
+
76
+ function pickDir(args) {
77
+ const flags = args.filter((a) => a.startsWith("--"));
78
+ const values = new Set(VALUE_FLAGS.map((n) => { const i = args.indexOf(`--${n}`); return i >= 0 ? i + 1 : -1; }).filter((i) => i >= 0));
79
+ const positional = args.filter((a, i) => !a.startsWith("--") && !values.has(i));
80
+ const explicit = positional.find(isDirArg);
81
+ const rest = positional.filter((a) => a !== explicit);
82
+ const found = locate({ explicit });
83
+ return { dir: found.dir, via: found.via, flags, rest };
84
+ }
85
+
86
+ async function main(argv) {
87
+ const [cmd, ...args] = argv;
88
+ switch (cmd) {
89
+ case undefined:
90
+ case "help":
91
+ case "--help":
92
+ case "-h":
93
+ console.log(HELP);
94
+ return 0;
95
+
96
+ case "version":
97
+ case "--version":
98
+ console.log(`evda tool schema ${SCHEMA_VERSION}`);
99
+ return 0;
100
+
101
+ case "init": {
102
+ const mod = await import("../create-evda.mjs");
103
+ return mod.main(args);
104
+ }
105
+
106
+ case "where": {
107
+ const found = locate({});
108
+ console.log(`${found.dir} (via ${found.via})`);
109
+ return 0;
110
+ }
111
+
112
+ case "use": {
113
+ const dir = args.find((a) => !a.startsWith("--"));
114
+ if (!dir) throw new EvdaError("use needs a directory", { exit: 2 });
115
+ const p = setPointer(dir);
116
+ console.log(`${p} -> ${resolve(dir)}`);
117
+ return 0;
118
+ }
119
+
120
+ case "doctor": {
121
+ const { dir, via } = pickDir(args);
122
+ return doctor(dir, { via }).exit;
123
+ }
124
+
125
+ case "reindex": {
126
+ const { dir, flags } = pickDir(args);
127
+ if (flags.includes("--adopt")) {
128
+ const manifest = readManifest(dir);
129
+ if (readVersion(dir) !== null) throw new EvdaError("REFUSED: .evda/version already exists; --adopt is only for a record that lost it.", { exit: 2 });
130
+ if (manifest.evda > SCHEMA_VERSION) throw new EvdaError(`REFUSED: evda.json declares ${manifest.evda}, newer than this tool (${SCHEMA_VERSION}); cannot vouch for a layout it does not know.`, { exit: 2 });
131
+ const evda = { dir, manifest, version: null };
132
+ const missing = resolveAll(evda).filter((r) => r.mapped && r.known && (!r.exists || (r.file && !r.fileExists)));
133
+ if (missing.length) throw new EvdaError(`REFUSED to adopt: mapped role(s) missing on disk: ${missing.map((r) => `${r.role} -> ${r.rel}`).join("; ")}`, { exit: 2 });
134
+ writeVersion(dir, manifest.evda);
135
+ console.log(`adopted: wrote .evda/version = ${manifest.evda} after every mapped role resolved`);
136
+ }
137
+ const evda = openEvda(dir);
138
+ const r = reindex(evda);
139
+ console.log(`reindex: ${r.ledger ? `${r.rows} row(s) in .evda/due.jsonl` : "no ledger role, .evda/due.jsonl is empty"}; digest ${r.digest ? "rebuilt" : "not written (AGENTS.md has no generated fence)"}`);
140
+ return 0;
141
+ }
142
+
143
+ case "due": {
144
+ const { dir } = pickDir(args);
145
+ const evda = openEvda(dir);
146
+ const expected = expectedIndex(evda);
147
+ if (!expected) { console.log("no ledger role in this record"); return 0; }
148
+ const actual = currentIndex(evda);
149
+ if (actual !== null && actual !== expected.text) console.error("note: .evda/due.jsonl is stale; showing the derived view. Run `evda reindex`.");
150
+ if (!expected.text) { console.log("nothing is due"); return 0; }
151
+ for (const line of expected.text.trimEnd().split("\n")) {
152
+ const r = JSON.parse(line);
153
+ console.log(`${r.due} ${r.id} [${r.status}] ${r.hypothesis}`);
154
+ }
155
+ return 0;
156
+ }
157
+
158
+ case "chain": {
159
+ const { dir, rest } = pickDir(args);
160
+ const id = rest.find((a) => /^H-\d{4}$/.test(a));
161
+ if (!id) throw new EvdaError("chain needs an id like H-0004");
162
+ const evda = openEvda(dir);
163
+ const ledger = readLedger(evda);
164
+ if (!ledger) throw new EvdaError("no ledger role in this record");
165
+ const c = chain(ledger.parsed, id);
166
+ if (!c) throw new EvdaError(`${id} is not in ${ledger.rel}`);
167
+ for (const e of c) console.log(`${e.id === id ? ">" : " "} ${e.id} [${e.statusWord}] ${e.title}`);
168
+ return 0;
169
+ }
170
+
171
+ case "current": {
172
+ const { dir } = pickDir(args);
173
+ const evda = openEvda(dir);
174
+ const ledger = readLedger(evda);
175
+ if (!ledger) throw new EvdaError("no ledger role in this record");
176
+ const cur = current(ledger.parsed);
177
+ if (!cur.length) console.log("no current hypotheses");
178
+ for (const e of cur) console.log(`${e.id} [${e.status}] ${e.title}`);
179
+ return 0;
180
+ }
181
+
182
+ case "moment": {
183
+ const { dir, rest } = pickDir(args);
184
+ const [from, to] = rest;
185
+ const evda = openEvda(dir);
186
+ console.log(JSON.stringify(moment(evda, { from, to: to ?? from }), null, 2));
187
+ return 0;
188
+ }
189
+
190
+ case "upgrade": {
191
+ const { dir } = pickDir(args);
192
+ const evda = openEvda(dir);
193
+ const a = agentsFile(evda);
194
+ if (!a) throw new EvdaError('no "agents" role in the mapping, so there is no operating layer to upgrade.');
195
+ const text = readFileSync(a.abs, "utf8");
196
+ const parts = splitFence(text);
197
+ if (!parts) throw new EvdaError(`${a.rel} has no evda:generated fence. The owner has taken the whole file; there is nothing the installer owns to replace, and nothing was changed.`);
198
+ const shipped = shippedGenerated();
199
+ if (parts.generated === shipped) { console.log("upgrade: the generated block already matches what this tool ships. Nothing outside it was read."); return 0; }
200
+ writeFileSync(a.abs, replaceFence(text, shipped));
201
+ reindex(evda);
202
+ console.log(`upgrade: replaced the generated block of ${a.rel} (${Buffer.byteLength(parts.generated)} -> ${Buffer.byteLength(shipped)} bytes). Everything outside the fence is untouched. Digest rebuilt.`);
203
+ return 0;
204
+ }
205
+
206
+ case "digest": {
207
+ const { dir } = pickDir(args);
208
+ const evda = openEvda(dir);
209
+ const d = expectedDigest(evda);
210
+ if (d === null) throw new EvdaError("the operating layer is unmapped, missing, or has no evda:generated fence, so there is nothing to digest.");
211
+ process.stdout.write(d);
212
+ return 0;
213
+ }
214
+
215
+ case "vector": {
216
+ // The conformance projection of one ledger file, for another implementation to diff against.
217
+ // evda vector <dir-with-ledger.md> print the projection as JSON
218
+ // evda vector <dir> --check compare to <dir>/expected.json, exit 1 on drift
219
+ const { vectorMain } = await import("../lib/vectorcli.mjs");
220
+ return vectorMain(args);
221
+ }
222
+
223
+ case "references": {
224
+ // Every reference in every record file, with the folder that governs it. With --resolve,
225
+ // each is asked of its source through the registry; a url is fetched, the rest say SKIPPED
226
+ // and why. Exit 1 when any reference is malformed, carries a secret, or has no connector.
227
+ //
228
+ // FALSIFIED 2026-09-16 on a scratch record whose record/people/sam.md carried two references,
229
+ // one with `password: hunter2` and one through a connector the folder did not declare ->
230
+ // "FAIL REFUSED: record/people/sam.md: references[0] carries a field named "password". ..."
231
+ // "FAIL record/people/sam.md contacts:2F1A-77 Sam Rivera (unresolved, never resolved)
232
+ // (record/people/folder.evda.json does not declare contacts)"
233
+ // "CHECKED 2 references", exit 1. The secret's value was not printed.
234
+ const { dir, flags } = pickDir(args);
235
+ const evda = openEvda(dir);
236
+ const doResolve = flags.includes("--resolve");
237
+ const t = flags.find((f) => f.startsWith("--timeout="));
238
+ const timeoutMs = t ? Number(t.slice("--timeout=".length)) : 5000;
239
+ const folders = folderManifests(evda);
240
+ let n = 0;
241
+ let bad = 0;
242
+ for (const r of resolveAll(evda).filter((x) => x.known && x.mapped && x.layer === "record" && x.exists && x.isDir)) {
243
+ for (const p of mdFiles(r.abs)) {
244
+ const rel = relative(dir, p);
245
+ const found = referencesIn(readFileSync(p, "utf8"), rel);
246
+ for (const pr of found.problems) { bad += 1; n += 1; console.log(`FAIL ${pr.message}`); }
247
+ const folder = folderFor(p, folders);
248
+ for (const ref of found.references) {
249
+ n += 1;
250
+ let result = null;
251
+ let gate = null;
252
+ if (!folder) { bad += 1; gate = `no ${FOLDER_MANIFEST} above it`; }
253
+ else if (!folder.connectors.includes(ref.scheme)) { bad += 1; gate = `${folder.rel} does not declare ${ref.scheme}`; }
254
+ if (doResolve) result = await resolveReference(ref, { connectors: folder ? folder.connectors : [], timeoutMs });
255
+ const shown = renderReference(ref, result);
256
+ console.log(`${gate ? "FAIL " : " "} ${rel} ${ref.ref} ${shown}${gate ? ` (${gate})` : ""}${result?.status === "resolved" ? ` resolved ${result.at}` : ""}`);
257
+ }
258
+ }
259
+ }
260
+ if (n === 0) console.log("SKIPPED references: no record file carries a references list");
261
+ else console.log(`CHECKED ${n} references${doResolve ? ", each asked of its source" : ""}`);
262
+ return bad ? 1 : 0;
263
+ }
264
+
265
+ case "folder": {
266
+ // evda folder <dir> --type T [--connectors a,b] [--force]
267
+ // Views are declared by editing the manifest: a declaration is an object with a path, not a
268
+ // name, so it has no comma-list flag. The scaffold writes `views: []`.
269
+ const positional = args.filter((a) => !a.startsWith("--"));
270
+ const target = positional[0];
271
+ if (!target) throw new EvdaError("folder needs a directory inside the record", { exit: 2 });
272
+ const opt = (name) => { const f = args.find((a) => a.startsWith(`--${name}=`)); return f ? f.slice(name.length + 3) : (args.includes(`--${name}`) ? args[args.indexOf(`--${name}`) + 1] : undefined); };
273
+ const type = opt("type");
274
+ if (!type) throw new EvdaError("folder needs --type", { exit: 2 });
275
+ const list = (v) => (v ? v.split(",").map((s) => s.trim()).filter(Boolean) : []);
276
+ const root = findRoot(resolve(target));
277
+ if (!root) throw new EvdaError(`REFUSED: no evda.json above ${resolve(target)}; the folder is not inside a record.`, { code: "NO_MANIFEST", exit: 2 });
278
+ const evda = openEvda(root);
279
+ const p = writeFolderManifest(evda, resolve(target), { type, connectors: list(opt("connectors")) }, { overwrite: args.includes("--force") });
280
+ console.log(`wrote ${relative(evda.dir, p)}`);
281
+ return 0;
282
+ }
283
+
284
+ case "grant": {
285
+ const { dir, rest } = pickDir(args);
286
+ const circle = rest[0];
287
+ if (!circle) throw new EvdaError("grant needs a circle name", { exit: 2 });
288
+ const to = flagValue(args, "to");
289
+ const kind = flagValue(args, "kind");
290
+ if (!to || !kind) throw new EvdaError(`grant needs --to <record-id> and --kind ${HOLDER_KINDS.join("|")}`, { exit: 2 });
291
+ const evda = openEvda(dir);
292
+ const p = grant(evda, { circle, to: { id: to, kind, name: flagValue(args, "name") }, role: flagValue(args, "role") ?? "viewer", by: flagValue(args, "by") }, { overwrite: args.includes("--force") });
293
+ console.log(`granted ${flagValue(args, "role") ?? "viewer"} on ${circle} to ${kind} ${to}: ${relative(evda.dir, p)}`);
294
+ return 0;
295
+ }
296
+
297
+ case "grants": {
298
+ const { dir } = pickDir(args);
299
+ const evda = openEvda(dir);
300
+ const gs = readGrants(evda);
301
+ if (gs === null) { console.log("SKIPPED grants: no grants role in this record"); return 0; }
302
+ let bad = 0;
303
+ for (const g of gs) {
304
+ for (const p of g.problems) { if (p.level === "fail") bad += 1; console.log(`${p.level === "fail" ? "FAIL " : "WARN "} ${p.message}`); }
305
+ if (g.principalId) console.log(` ${g.principalId} ${g.role ?? "?"} on ${g.circle ?? "?"} ${g.holder ? `${g.holder.kind}${g.holder.name ? ` ${g.holder.name}` : ""}` : "holder unknown"} ${g.rel}`);
306
+ }
307
+ if (gs.length === 0) console.log("SKIPPED grants: no grant files");
308
+ else console.log(`CHECKED ${gs.length} grants`);
309
+ return bad ? 1 : 0;
310
+ }
311
+
312
+ case "revoke": {
313
+ const { dir, rest } = pickDir(args);
314
+ const circle = rest[0];
315
+ const to = flagValue(args, "to");
316
+ if (!circle || !to) throw new EvdaError("revoke needs a circle name and --to <record-id>", { exit: 2 });
317
+ const evda = openEvda(dir);
318
+ console.log(revoke(evda, { circle, id: to }) ? `revoked ${to} on ${circle}` : `nothing to revoke: ${to} had no grant on ${circle}`);
319
+ return 0;
320
+ }
321
+
322
+ case "access": {
323
+ const { dir, rest } = pickDir(args);
324
+ const circle = rest[0];
325
+ const as = flagValue(args, "as");
326
+ if (!circle) throw new EvdaError("access needs a circle name", { exit: 2 });
327
+ const evda = openEvda(dir);
328
+ const a = access(evda, circle, as ?? null);
329
+ console.log(`${a.allowed ? "ALLOWED" : "REFUSED"} ${as ?? "(nobody named)"} on ${circle}: ${a.why}`);
330
+ return a.allowed ? 0 : 2;
331
+ }
332
+
333
+ case "migrate": {
334
+ const { dir } = pickDir(args);
335
+ const r = await migrate(dir, { out: console.log });
336
+ console.log(r.ran.length ? `migrated ${r.from} -> ${r.to}` : `nothing to do: on disk ${r.from}, tool ${SCHEMA_VERSION}`);
337
+ return 0;
338
+ }
339
+
340
+ case "status": {
341
+ const { dir, via } = pickDir(args);
342
+ const evda = openEvda(dir);
343
+ const m = evda.manifest;
344
+ const h = holderOf(evda);
345
+ console.log(`${m.name ?? "(unnamed)"} schema ${m.evda} held by a ${h.kind}${h.id ? ` (${h.id})` : ""} layers [${m.layers.join(", ")}] ${evda.dir} (via ${via})`);
346
+ for (const r of resolveAll(evda)) {
347
+ if (!r.known) { console.log(` ${r.role.padEnd(16)} -> ${r.rel} (unknown role, preserved)`); continue; }
348
+ if (!r.mapped) { console.log(` ${r.role.padEnd(16)} absent`); continue; }
349
+ console.log(` ${r.role.padEnd(16)} -> ${r.rel} ${r.exists ? "" : "MISSING"}`);
350
+ }
351
+ const ledger = readLedger(evda);
352
+ if (ledger) {
353
+ const es = ledger.parsed.entries;
354
+ const last = es[es.length - 1];
355
+ console.log(` ledger: ${es.length} entries, ${current(ledger.parsed).length} current, last ${last ? last.id : "none"}`);
356
+ }
357
+ const expected = expectedIndex(evda);
358
+ if (expected && expected.text) console.log(` due: ${expected.text.trimEnd().split("\n").length} row(s); \`evda due\` lists them`);
359
+ console.log(` pointer: ${pointerPath()}`);
360
+ return 0;
361
+ }
362
+
363
+ default:
364
+ console.error(`unknown command: ${cmd}\n`);
365
+ console.log(HELP);
366
+ return 2;
367
+ }
368
+ }
369
+
370
+ // Through realpath: an installed bin is a symlink and resolve() does not follow it (see the same
371
+ // guard in create-evda.mjs, and test/bins.test.mjs).
372
+ if (process.argv[1] && isEntry(process.argv[1], import.meta.url)) {
373
+ main(process.argv.slice(2)).then(
374
+ (code) => process.exit(code),
375
+ (e) => {
376
+ console.error(e instanceof EvdaError ? e.message : e.stack ?? String(e));
377
+ process.exit(e instanceof EvdaError ? e.exit : 1);
378
+ }
379
+ );
380
+ }
381
+
382
+ export { main };