yay-layer 1.0.0-rc.1

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.
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+ // P4 — long-lived assurance helpers (pure logic; the CLI/dashboard wire them up):
3
+ // • reverifyDiff — compare a stored attestation's verdicts to a fresh run (verifier upgrades
4
+ // APPEND a new attestation, never rewrite — "Green is an event", D5).
5
+ // • integrityWitness — cross-check the append-only ledgers against each other and the tree
6
+ // (git vs ledger vs spec-archive): does the record still describe reality?
7
+ // • earnedAutonomy — turn the rejection history (P3) into per-category trust metrics: where the
8
+ // agent's delegated work is reliably ratified vs. repeatedly rejected.
9
+
10
+ // ── reverify diff ────────────────────────────────────────────────────────────────
11
+ // prevAtt.evidence and newVerObj.evidence are per-Cell { state, proven } maps. Returns the verdict
12
+ // changes between them (a Cell moving YELLOW→GREEN is an *improvement* from a better verifier; a
13
+ // GREEN→RED is a *regression* a capability bump surfaced). Never mutates either input.
14
+ function reverifyDiff(prevAtt, newVerObj) {
15
+ const a = (prevAtt && prevAtt.evidence) || {};
16
+ const b = (newVerObj && newVerObj.evidence) || {};
17
+ const rank = { PINK: 0, RED: 1, UNSIGNED: 2, YELLOW: 3, GREEN: 4 };
18
+ const ids = Object.keys({ ...a, ...b }).sort();
19
+ const changes = [];
20
+ let improved = 0, regressed = 0;
21
+ for (const id of ids) {
22
+ const from = a[id] ? a[id].state : '(absent)';
23
+ const to = b[id] ? b[id].state : '(gone)';
24
+ const fromProven = !!(a[id] && a[id].proven), toProven = !!(b[id] && b[id].proven);
25
+ if (from === to && fromProven === toProven) continue;
26
+ const dir = ((rank[to] != null ? rank[to] : -1) - (rank[from] != null ? rank[from] : -1));
27
+ if (dir > 0 || (from === to && !fromProven && toProven)) improved++;
28
+ else if (dir < 0) regressed++;
29
+ changes.push({ cell: id, from, to, fromProven, toProven });
30
+ }
31
+ return {
32
+ capabilityChanged: !!(prevAtt && newVerObj && prevAtt.capability !== newVerObj.capability),
33
+ fromCapability: prevAtt ? prevAtt.capability : null,
34
+ toCapability: newVerObj ? newVerObj.capability : null,
35
+ changes, improved, regressed,
36
+ };
37
+ }
38
+
39
+ // ── integrity witness (git/tree vs ledger vs spec-archive) ─────────────────────────
40
+ // A witness answers "does the record still describe reality, and is the record internally sound?"
41
+ // It takes already-loaded state (so it's pure/testable) and returns { ok, problems, checks }.
42
+ // deps: {
43
+ // ledger, // attestation ledger { entries:[...] }
44
+ // loadAttestation, // (hash) => validated attestation | null (tamper check baked in)
45
+ // currentCodeTreeHash,
46
+ // approvals, // lock.approvals
47
+ // hasSpecObject, // (specHash) => bool (is the as-signed spec archived? P1)
48
+ // }
49
+ function integrityWitness(deps) {
50
+ const problems = [];
51
+ const led = (deps.ledger && deps.ledger.entries) || [];
52
+ // 1) attestation chain: each entry references the previous hash, and each stored object validates.
53
+ let chainOk = true, prev = 'genesis', validated = 0;
54
+ for (const e of led) {
55
+ if (e.prev !== prev) { chainOk = false; problems.push(`attestation ${String(e.hash).slice(0, 12)}: broken chain (prev ${String(e.prev).slice(0, 8)} ≠ expected ${String(prev).slice(0, 8)})`); }
56
+ const att = deps.loadAttestation ? deps.loadAttestation(e.hash) : null;
57
+ if (!att) problems.push(`attestation ${String(e.hash).slice(0, 12)}: object missing or fails signature/tamper check`);
58
+ else validated++;
59
+ prev = e.hash;
60
+ }
61
+ // 2) git/tree vs ledger: does the newest attestation still describe the code on disk?
62
+ const last = led.length ? led[led.length - 1] : null;
63
+ const covered = !!(last && deps.currentCodeTreeHash && last.codeTreeHash === deps.currentCodeTreeHash);
64
+ if (last && !covered) problems.push(`the latest attestation (${String(last.hash).slice(0, 12)}) no longer matches the current code — re-run \`yay attest\` / \`yay reverify\``);
65
+ // 3) spec-archive completeness: every signed spec identity should be archived (P1), so "as signed"
66
+ // reconstruction never depends on git surviving.
67
+ let missingSpecs = 0, checkedSpecs = 0;
68
+ if (deps.hasSpecObject) {
69
+ const seen = new Set();
70
+ for (const ap of deps.approvals || []) {
71
+ for (const id of Object.keys(ap.items || {})) {
72
+ const h = ap.items[id]; if (!h || seen.has(h)) continue; seen.add(h); checkedSpecs++;
73
+ if (!deps.hasSpecObject(h)) missingSpecs++;
74
+ }
75
+ }
76
+ if (missingSpecs) problems.push(`${missingSpecs} signed spec revision(s) are not in the archive (history could blank if git is lost) — re-sign or run a spec re-archive`);
77
+ }
78
+ return {
79
+ ok: problems.length === 0,
80
+ problems,
81
+ checks: { attestations: led.length, attestationsValidated: validated, chainOk, latestCovered: covered, specsChecked: checkedSpecs, specsMissing: missingSpecs },
82
+ };
83
+ }
84
+
85
+ // ── earned-autonomy metrics (from rejection + delegation history) ─────────────────
86
+ // "refactor delegated changes — 97% ratified; dependency changes — 31% rejected → exclude by default."
87
+ // Approximate but honest: delegated = autoApproved approvals; ratified ≈ a Cell that was delegated and
88
+ // later carries a real human approval; rejected = recorded rejection events (P3), by category.
89
+ function earnedAutonomy(rejections, approvals) {
90
+ const rej = (rejections && rejections.events ? rejections.events : rejections) || [];
91
+ const aps = approvals || [];
92
+ // per-Cell: was it ever delegated? ever human-signed after?
93
+ const delegatedCell = new Set(), humanCell = new Set();
94
+ for (const ap of aps) {
95
+ const ids = Object.keys(ap.items || {});
96
+ if (ap.autoApproved) ids.forEach((id) => delegatedCell.add(id));
97
+ else ids.forEach((id) => humanCell.add(id));
98
+ }
99
+ let ratified = 0; for (const id of delegatedCell) if (humanCell.has(id)) ratified++;
100
+ const byCategory = {};
101
+ let rejectedCells = 0;
102
+ for (const e of rej) {
103
+ if (e.type && e.type !== 'reject') continue;
104
+ const cat = String(e.category || 'other').toLowerCase();
105
+ const n = (e.cells || []).length || 1;
106
+ byCategory[cat] = (byCategory[cat] || 0) + n;
107
+ rejectedCells += n;
108
+ }
109
+ const delegated = delegatedCell.size;
110
+ const decided = ratified + rejectedCells;
111
+ const suggestions = [];
112
+ for (const cat of Object.keys(byCategory)) {
113
+ if (byCategory[cat] >= 3) suggestions.push(`"${cat}" changes have been rejected ${byCategory[cat]}× — consider excluding them from grants by default`);
114
+ }
115
+ return {
116
+ delegated, ratified, rejected: rejectedCells, byCategory,
117
+ ratifiedRate: decided ? +(ratified / decided).toFixed(3) : null,
118
+ suggestions,
119
+ };
120
+ }
121
+
122
+ module.exports = { reverifyDiff, integrityWitness, earnedAutonomy };
package/src/attest.js ADDED
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+ // P2 — VERIFIER ATTESTATION. The third cryptographic identity (after artifact hashes and the
3
+ // human phone signature): the machine verifier signs its OWN verdict with its OWN key, so "Green"
4
+ // stops being a local, unfalsifiable claim and becomes a portable, verifiable object — "Green
5
+ // under THIS verifier + environment at this time" (see docs 02, 03; decisions D3, D5, D13, D16).
6
+ //
7
+ // Separation of duties (D1): the verifier key is DISTINCT from any human/owner key and from the
8
+ // machine grant key. It is project/CI-scoped and MACHINE-HELD (private half gitignored in
9
+ // .yaylayer/keys/, never committed, NEVER shipped in the npm package — D11); only its PUBLIC key
10
+ // is committed (config.verifier), so anyone can verify an attestation without being able to forge
11
+ // one. A fresh clone / a CI runner without the secret can VERIFY attestations but cannot MINT them.
12
+ //
13
+ // "Green is an event" (D5): attestations are append-only. Each references the previous one's hash;
14
+ // a better verifier appends new attestations (P4 `yay reverify`), never rewrites old ones. The
15
+ // verifier carries a CAPABILITY VERSION (PATCH/MINOR/MAJOR) recorded in every attestation, so a
16
+ // capability bump is a first-class, dated event.
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const os = require('os');
21
+ const C = require('./crypto');
22
+ const { canonical } = require('./util');
23
+ const rosterMod = require('./roster');
24
+
25
+ // The verifier's capability version + fingerprint live in src/capability.js, DERIVED from the live
26
+ // provers/effect-nets/checks so a forgotten version bump can't silently claim more than the verifier
27
+ // can do (assertCapability). Every attestation records both the declared version and the fingerprint.
28
+ const { CAPABILITY, capabilityFingerprint } = require('./capability');
29
+
30
+ function pkgVersion() {
31
+ try { return require('../package.json').version || '0.0.0'; } catch (_) { return '0.0.0'; }
32
+ }
33
+
34
+ function verifierKeyPath(p) { return path.join(p.keys, 'verifier.json'); }
35
+ function attestPath(p) { return path.join(p.yay, 'attest.json'); }
36
+ function attDir(p) { return path.join(p.yay, 'attestations'); }
37
+ function attFile(p, hash) { return path.join(attDir(p), String(hash) + '.json'); }
38
+
39
+ // Ensure a project verifier identity exists. The private half lives machine-side (gitignored keys/);
40
+ // the public half is mirrored into config.verifier (committed) as the project's verifier of record.
41
+ // `create` false → return null instead of generating (used by pure verification / read paths).
42
+ function verifierIdentity(p, config, opts) {
43
+ const create = !opts || opts.create !== false;
44
+ const kp = verifierKeyPath(p);
45
+ let rec = null;
46
+ try { rec = JSON.parse(fs.readFileSync(kp, 'utf8')); } catch (_) { rec = null; }
47
+ if (!rec) {
48
+ if (!create) return null;
49
+ const gk = C.generateKeypair();
50
+ rec = { pub: gk.pubB64, priv: Buffer.from(gk.privDer).toString('base64'), createdAt: new Date().toISOString() };
51
+ fs.mkdirSync(p.keys, { recursive: true });
52
+ fs.writeFileSync(kp, JSON.stringify(rec, null, 2) + '\n', { mode: 0o600 });
53
+ }
54
+ return { pub: rec.pub, privDer: Buffer.from(rec.priv, 'base64'), fp: rosterMod.fingerprint(rec.pub) };
55
+ }
56
+
57
+ // The public verifier of record for this project: config.verifier.pub if pinned (committed),
58
+ // else the local key's public half. Used to VERIFY attestations (needs no private key).
59
+ function verifierPub(p, config) {
60
+ if (config && config.verifier && config.verifier.pub) return config.verifier.pub;
61
+ const id = verifierIdentity(p, config, { create: false });
62
+ return id ? id.pub : null;
63
+ }
64
+
65
+ // Pin the local verifier's PUBLIC key into config (committed) as the project verifier of record.
66
+ // Returns true if config changed (caller writes it).
67
+ function pinVerifier(config, id) {
68
+ config.verifier = config.verifier || {};
69
+ const changed = config.verifier.pub !== id.pub || config.verifier.capability !== CAPABILITY;
70
+ config.verifier.pub = id.pub;
71
+ config.verifier.fp = id.fp;
72
+ config.verifier.capability = CAPABILITY;
73
+ return changed;
74
+ }
75
+
76
+ // ── the canonical Verification object (what gets hashed + signed) ──────────────────────────────
77
+ // A deterministic digest of exactly the inputs and outputs of a verification run:
78
+ // specSetHash — the set of signed spec identities (per-Cell specHash)
79
+ // codeTreeHash — the exact code that was judged (per-Cell body hash)
80
+ // rulesetHash — the signing/inertness policy in force (verdicts depend on it)
81
+ // evidenceHash — the per-Cell verdict + whether it was machine-proven
82
+ // result — the gate summary (passed + colour counts)
83
+ // env — recorded for determinism honesty (D16): same verifier can differ across
84
+ // environments only on timing-sensitive cases; the attestation says so.
85
+ // The leaf Cells (specs, not modules) in stable order — the unit of both spec-set and code-tree.
86
+ function leafIds(manifest) {
87
+ const cells = manifest.cells || {};
88
+ return Object.keys(cells).filter((id) => !(cells[id].contains && cells[id].contains.length)).sort();
89
+ }
90
+
91
+ // Hash of the exact code that was (or would be) judged — per-leaf-Cell body. Cheap: needs only the
92
+ // manifest, so callers (e.g. `yay sign`) can test "does an attestation cover this exact tree?"
93
+ // without re-running the full verifier.
94
+ function codeTreeHashOf(manifest) {
95
+ const cells = manifest.cells || {};
96
+ const codeTree = {};
97
+ for (const id of leafIds(manifest)) codeTree[id] = C.sha256(String(cells[id].unitBody || ''));
98
+ return C.sha256(canonical(codeTree));
99
+ }
100
+
101
+ function buildVerification(manifest, verified, opts) {
102
+ opts = opts || {};
103
+ const cells = manifest.cells || {};
104
+ const results = (verified && verified.results) || {};
105
+ const leaves = leafIds(manifest);
106
+
107
+ const specSet = {};
108
+ const codeTree = {};
109
+ const evidence = {};
110
+ for (const id of leaves) {
111
+ const c = cells[id];
112
+ specSet[id] = c.specHash || null;
113
+ codeTree[id] = C.sha256(String(c.unitBody || ''));
114
+ const r = results[id] || {};
115
+ evidence[id] = { state: r.state || 'UNSIGNED', proven: !!r.proven };
116
+ // Record the branch-coverage boundary of a proof, so the signed verdict is honest about
117
+ // how much of the code its inputs actually exercised (not just "proven").
118
+ if (r.coverage && r.coverage.total) evidence[id].branches = { exercised: r.coverage.exercised, total: r.coverage.total };
119
+ }
120
+ const counts = (verified && verified.counts) || {};
121
+ const result = {
122
+ passed: !!(verified && verified.passed),
123
+ counts: { GREEN: counts.GREEN || 0, YELLOW: counts.YELLOW || 0, RED: counts.RED || 0, UNSIGNED: counts.UNSIGNED || 0, PINK: counts.PINK || 0 },
124
+ proven: counts.proven || 0,
125
+ };
126
+ return {
127
+ v: 1,
128
+ kind: 'verification',
129
+ project: (opts.config && opts.config.project) || null,
130
+ capability: CAPABILITY,
131
+ capabilityFingerprint: capabilityFingerprint(), // binds the verdict to the EXACT detector set that produced it
132
+ verifierPkg: pkgVersion(),
133
+ specSetHash: C.sha256(canonical(specSet)),
134
+ codeTreeHash: C.sha256(canonical(codeTree)),
135
+ rulesetHash: C.sha256(canonical(opts.policy || { rules: [] })),
136
+ rootFp: (verified && verified.rootFp) || null,
137
+ evidenceHash: C.sha256(canonical(evidence)),
138
+ evidence, // per-Cell {state,proven} — small, self-describing, and lets `yay reverify` diff verdicts across capability versions
139
+ cells: leaves.length,
140
+ result,
141
+ env: { node: process.version, platform: os.platform() + '/' + os.arch() },
142
+ at: opts.at || new Date().toISOString(),
143
+ };
144
+ }
145
+
146
+ // Sign a verification object with the verifier key → a self-contained attestation. `hash` and the
147
+ // signature both cover the SAME canonical bytes of the bare verification object, so a verifier
148
+ // re-canonicalises {everything except hash/verifier/signature} and checks both.
149
+ function signAttestation(verObj, id) {
150
+ const bytes = canonical(verObj);
151
+ const hash = C.sha256(bytes);
152
+ const signature = C.sign(bytes, id.privDer);
153
+ return Object.assign({}, verObj, { hash, verifier: { pub: id.pub, fp: id.fp }, signature });
154
+ }
155
+
156
+ // Recompute the canonical bytes of an attestation's verification core (strip the wrapper fields).
157
+ function coreBytes(att) {
158
+ const core = Object.assign({}, att);
159
+ delete core.hash; delete core.verifier; delete core.signature; delete core.prev;
160
+ return canonical(core);
161
+ }
162
+
163
+ // Validate an attestation: its hash matches its core, the signature verifies under its embedded
164
+ // verifier key, and (if a project verifier is pinned) that key IS the project's verifier of record.
165
+ function verifyAttestation(att, expectedPub) {
166
+ if (!att || !att.verifier || !att.signature) return { ok: false, reason: 'not an attestation' };
167
+ const bytes = coreBytes(att);
168
+ if (C.sha256(bytes) !== att.hash) return { ok: false, reason: 'hash does not match content (tampered)' };
169
+ if (!C.verify(bytes, att.signature, att.verifier.pub)) return { ok: false, reason: 'bad verifier signature' };
170
+ if (expectedPub && att.verifier.pub !== expectedPub) return { ok: false, reason: 'signed by a different verifier than the project verifier of record' };
171
+ return { ok: true };
172
+ }
173
+
174
+ // Append-only attestation ledger (committed). Each entry references the previous hash → a chain
175
+ // anchored by the project verifier. The full attestation object is stored content-addressed in
176
+ // objects/ (keyed by its own hash); the ledger keeps the ordered index.
177
+ function loadLedger(p, config) {
178
+ const led = (function () { try { return JSON.parse(fs.readFileSync(attestPath(p), 'utf8')); } catch (_) { return null; } })();
179
+ return led || { project: (config && config.project) || null, capability: CAPABILITY, entries: [] };
180
+ }
181
+
182
+ function appendAttestation(p, config, att) {
183
+ const led = loadLedger(p, config);
184
+ const prev = led.entries.length ? led.entries[led.entries.length - 1].hash : 'genesis';
185
+ att.prev = prev;
186
+ // Persist the full attestation (committed), keyed by its own attestation hash. The tamper check
187
+ // is verifyAttestation() — stronger than a raw sha256(content) check because it also re-validates
188
+ // the verifier signature. The ledger keeps the ordered, chained index.
189
+ fs.mkdirSync(attDir(p), { recursive: true });
190
+ fs.writeFileSync(attFile(p, att.hash), JSON.stringify(att, null, 2) + '\n');
191
+ led.entries.push({ hash: att.hash, at: att.at, passed: att.result.passed, capability: att.capability, codeTreeHash: att.codeTreeHash, prev });
192
+ led.capability = CAPABILITY;
193
+ fs.mkdirSync(path.dirname(attestPath(p)), { recursive: true });
194
+ fs.writeFileSync(attestPath(p), JSON.stringify(led, null, 2) + '\n');
195
+ return att;
196
+ }
197
+
198
+ // Load a stored attestation by its hash, re-validating signature + integrity before returning it
199
+ // (null if missing or tampered — never hand back an unverified attestation).
200
+ function loadAttestation(p, config, hash) {
201
+ let att = null;
202
+ try { att = JSON.parse(fs.readFileSync(attFile(p, hash), 'utf8')); } catch (_) { return null; }
203
+ const chk = verifyAttestation(att, verifierPub(p, config));
204
+ return chk.ok ? att : null;
205
+ }
206
+
207
+ function latestEntry(p, config) {
208
+ const led = loadLedger(p, config);
209
+ return led.entries.length ? led.entries[led.entries.length - 1] : null;
210
+ }
211
+
212
+ module.exports = {
213
+ CAPABILITY, verifierKeyPath, attestPath, attDir, attFile, verifierIdentity, verifierPub, pinVerifier,
214
+ buildVerification, codeTreeHashOf, leafIds, signAttestation, verifyAttestation, coreBytes,
215
+ loadLedger, appendAttestation, loadAttestation, latestEntry,
216
+ };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+ // VERIFIER CAPABILITY — the verifier's own versioned identity, DERIVED from what it can actually
3
+ // detect and prove, so the declared version can't drift from reality.
4
+ //
5
+ // The problem this closes: `CAPABILITY` is a semver a human bumps by hand. If someone adds a prover,
6
+ // an effect net, or a check but forgets to bump it, an attestation would claim capability 1.0.0 while
7
+ // the verifier is really doing more (or, after a semantics change, something different) — a silent,
8
+ // unversioned claim. So we compute a FINGERPRINT over a machine-derived descriptor of the live
9
+ // capabilities and REGISTER the expected fingerprint per version. `assertCapability()` compares the
10
+ // two; `yay attest` refuses on drift and tells you to bump + re-register. A shipped package never
11
+ // drifts (its code and registry match); only a *modified* verifier does — exactly when we want the
12
+ // guard to fire.
13
+
14
+ const { sha256 } = require('./crypto');
15
+ const { canonical } = require('./util');
16
+ const { ADAPTERS } = require('./prove');
17
+ const { effectNetDescriptor } = require('./verify');
18
+
19
+ // Declared capability version (PATCH = bug fix, same verdicts · MINOR = new detectors/provers, a
20
+ // verdict may improve · MAJOR = a semantics change that can flip existing verdicts). Bump this AND
21
+ // re-register the fingerprint (below) whenever `describeCapability()` changes.
22
+ const CAPABILITY = '1.2.0';
23
+
24
+ // Checks the verifier runs on a passing Cell, and the policy rule kinds it understands. Listed
25
+ // explicitly (and hashed) so adding/removing one is a visible, version-forcing change. Provers and
26
+ // effect nets are pulled from the live code, so those shift the fingerprint on their own.
27
+ const CHECKS = ['branch-coverage', 'ensures-prover', 'inertness', 'jsx-render-prover', 'literal-seeding', 'mutation-grading', 'predicate-provenance'];
28
+ const POLICY_KINDS = ['coverage-full', 'ignore-source', 'inert-level', 'non-delegable', 'predicate-declared', 'required-signer', 'reverify-latest'];
29
+
30
+ // A structured, machine-derived description of everything the verifier can currently do.
31
+ function describeCapability() {
32
+ return {
33
+ v: 1,
34
+ provers: ADAPTERS.map((a) => a.name).sort(), // ← live: adding an adapter changes this
35
+ effectNets: effectNetDescriptor(), // ← live: adding a net / signal changes this
36
+ checks: CHECKS.slice().sort(),
37
+ policyKinds: POLICY_KINDS.slice().sort(),
38
+ };
39
+ }
40
+
41
+ // A stable hash over the descriptor — the verifier's capability fingerprint.
42
+ function capabilityFingerprint() { return sha256(canonical(describeCapability())); }
43
+
44
+ // The fingerprint expected for each declared version. The entry for the CURRENT CAPABILITY must
45
+ // equal the live fingerprint; if it doesn't, the code changed without a version bump.
46
+ const REGISTERED = {
47
+ '1.0.0': '1579c1ec83344596b4e3de93162ac7062d9846e0bd9793c7969b1fe3064e1aa1',
48
+ '1.1.0': '1d2a74604dcffc789bf7de6ee98f9368fee68241ea7aafcb87c57c858167388c',
49
+ '1.2.0': 'f29468bffd24b0f93529bd1daa6b1f98e96dce9df1b55993d44b4055c9599d75',
50
+ };
51
+
52
+ // Compare the live fingerprint to the one registered for the declared version.
53
+ function assertCapability() {
54
+ const actual = capabilityFingerprint();
55
+ const expected = REGISTERED[CAPABILITY] || null;
56
+ return { ok: expected === actual, declared: CAPABILITY, expected, actual, drift: expected !== actual };
57
+ }
58
+
59
+ module.exports = { CAPABILITY, describeCapability, capabilityFingerprint, assertCapability, REGISTERED };
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+ // Write the YayLayer Constitution into the instruction file that a given AI coding
3
+ // harness auto-reads, so a fresh session in this repo follows YayLayer from the
4
+ // very first file. The Constitution text is the single source (../CONSTITUTION.md);
5
+ // we just place it where each tool looks.
6
+ //
7
+ // Writes are marker-wrapped and idempotent: the block between YAYLAYER:BEGIN/END is
8
+ // managed by `yay` (re-running updates just that block); anything outside it is the
9
+ // user's and is preserved.
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const BEGIN = '<!-- YAYLAYER:BEGIN (managed by `yay` — re-run `yay constitution` to update; edits inside are overwritten) -->';
15
+ const END = '<!-- YAYLAYER:END -->';
16
+
17
+ // File-based, auto-loaded instruction conventions per harness. `note` flags how
18
+ // standard each is — conventions move fast, so verify against your tool's docs.
19
+ const HARNESSES = [
20
+ { key: 'claude', label: 'Claude Code', path: 'CLAUDE.md', note: 'auto-loaded at repo root' },
21
+ { key: 'agents', label: 'AGENTS.md (Codex CLI + cross-tool standard)', path: 'AGENTS.md', note: 'read by OpenAI Codex CLI and a growing set of tools' },
22
+ { key: 'copilot', label: 'GitHub Copilot', path: '.github/copilot-instructions.md', note: 'repo custom instructions' },
23
+ { key: 'cursor', label: 'Cursor', path: '.cursorrules', note: 'legacy single-file (modern: .cursor/rules/*.mdc)' },
24
+ { key: 'windsurf', label: 'Windsurf', path: '.windsurfrules', note: 'root rules file' },
25
+ { key: 'cline', label: 'Cline', path: '.clinerules', note: 'root rules file' },
26
+ { key: 'gemini', label: 'Gemini CLI', path: 'GEMINI.md', note: 'auto-loaded at repo root' },
27
+ { key: 'generic', label: 'CONSTITUTION.md (wire into any tool yourself)', path: 'CONSTITUTION.md', note: 'plain copy; point your tool at it' },
28
+ ];
29
+
30
+ // The approval command depends on how THIS project signs (chosen at `yay init`):
31
+ // a phone-signed project must tell the AI to have the human approve on their phone.
32
+ // `yay sign` auto-uses the project's established method, so the command is always
33
+ // just `yay sign` — the note explains where the signature actually happens.
34
+ function signGuidance(method) {
35
+ if (method === 'phone') {
36
+ return { cmd: '`yay sign`', note: 'This project signs on a **phone**: `yay sign` automatically opens the phone approval (prints a QR/URL) and the signing key never touches this machine. You cannot sign — only the human’s phone can.' };
37
+ }
38
+ if (method === 'local') {
39
+ return { cmd: '`yay sign`', note: 'This project signs with a **local key**: `yay sign` prompts for the human’s passphrase.' };
40
+ }
41
+ return { cmd: '`yay sign`', note: '`yay sign` uses whichever signing method this project is set up with.' };
42
+ }
43
+
44
+ function header(method) {
45
+ const g = signGuidance(method);
46
+ const waits = method === 'phone';
47
+ return [
48
+ '# This project is built under YayLayer',
49
+ '',
50
+ 'Follow the YayLayer Constitution below **exactly, from the very first file**.',
51
+ '',
52
+ '**The loop for every observable change:**',
53
+ '1. Write/update the spec block(s) FIRST — no implementation code yet.',
54
+ '2. Draft a one-paragraph **Brief** — what the human asked you to build, in your own',
55
+ ' fine-tuned words (not their verbatim text) — and present the change-set headed by it,',
56
+ ' with the colour you expect each Cell to earn. Then request approval by running',
57
+ ` \`yay sign --brief "<that paragraph>"\` yourself. ${waits ? 'It BLOCKS until the human approves on their phone, where they can EDIT the brief before signing (allow a few minutes; use a long command timeout).' : 'The human enters their passphrase to sign.'}`,
58
+ `3. **The moment ${g.cmd} returns a completed signature, continue on your own** — implement the code`,
59
+ ' to match the signed spec, then run `yay verify` and report the result. Do NOT stop to ask',
60
+ ' "should I implement now?" — a returned signature IS the go-ahead.',
61
+ '4. When `yay verify` passes, COMMIT the code and `.yaylayer/` together in ONE commit',
62
+ ' (e.g. `git add -A && git commit -m "C-xxx: <intent> (signed)"`). This makes the seal durable',
63
+ ' (the CI gate reads committed state) and gives the next spec change a clean before/after diff.',
64
+ ' Do NOT `git push` unless the human asks.',
65
+ '5. If approval fails, is declined, or times out, STOP and ask — never implement unapproved specs.',
66
+ '',
67
+ '**When the human adds new requests before signing the pending brief:** decide by coherence, and keep one brief = one coherent intent.',
68
+ '- If the additions BELONG to the same brief (logically part of the same intent), SUGGEST folding them in: cancel the pending approval, add the new spec(s), and re-present ONE updated brief covering everything, then sign.',
69
+ '- If they are a DIFFERENT concern, ask the human to SIGN (or decline) the current brief FIRST, then start the new concern as its own separate brief.',
70
+ 'Never mix unrelated concerns into one brief, and never leave a stale pending approval hanging.',
71
+ '',
72
+ '**Specs are never perfect — treat `yay verify` as a spec-STRENGTHENING loop, not just a pass/fail gate.**',
73
+ 'When it flags a Cell as weak — `ensures` not machine-verified (prose), inputs (`in:`) under-declared, a',
74
+ 'dangling reference, a unit-name mismatch, or a prover/adversary counterexample — do NOT leave it green.',
75
+ 'Propose a STRONGER spec (a checkable `ensures` as a boolean JS expression over `out` and the inputs;',
76
+ 'tighter `in:` domains; the corrected clause), present it, get it RE-SIGNED, then reconcile the code.',
77
+ 'For SIDE-EFFECTING code (canvas/DOM/IO), declare its effect surface with `records: <param>` and write',
78
+ '`ensures` over the recorded trace — `calls(name)` (arg-arrays), `sets(name)` (assigned values),',
79
+ '`didCall(name)`, `didSet(name, value)` — so effects become machine-checkable instead of unprovable.',
80
+ 'A signed-but-unproven Cell is a to-do, not a finish line. Run `yay adversary` to hunt weak spots.',
81
+ '',
82
+ `Never sign on the human's behalf${waits ? ' (only their phone holds the key — you cannot)' : ''}. ${g.note}`.trim(),
83
+ '',
84
+ '---',
85
+ '',
86
+ '',
87
+ ].join('\n');
88
+ }
89
+
90
+ function constitutionText() {
91
+ return fs.readFileSync(path.join(__dirname, '..', 'CONSTITUTION.md'), 'utf8').trim();
92
+ }
93
+ // The project's own tag pool, embedded so the AI sees the ACTUAL tags to use (Article 13).
94
+ function tagPoolBlock(root) {
95
+ try {
96
+ const o = JSON.parse(fs.readFileSync(path.join(root, '.yaylayer', 'tags.json'), 'utf8'));
97
+ if (o && Array.isArray(o.tags) && o.tags.length) {
98
+ const d = o.descriptions || {};
99
+ const hasD = o.tags.some((t) => d[t]);
100
+ const body = hasD
101
+ ? o.tags.map((t) => '- `' + t + '`' + (d[t] ? ' — ' + d[t] : '')).join('\n')
102
+ : o.tags.map((t) => '`' + t + '`').join(' · ');
103
+ return '\n\n---\n\n**Project tag pool** (Article 13) — tag every Brief with 1–3 of these, via `yay sign --tags "…"`:\n\n' + body + '\n';
104
+ }
105
+ } catch (_) {}
106
+ return '';
107
+ }
108
+ // Cell-id shard directive (Article 2). The literal shard is NOT baked in here — the constitution is
109
+ // committed and shared, but each working copy needs its OWN shard so ids never collide on merge. So
110
+ // the shard lives in gitignored .yaylayer/local.json; this tells the AI to look it up per clone.
111
+ function shardBlock() {
112
+ return '\n\n---\n\n**Cell-id shard** (Article 2) — mint every NEW Cell as `C-<shard>-<n>`, where ' +
113
+ '`<shard>` is THIS working copy\'s shard: run `yay id` to get it (unique to your clone, so ids from ' +
114
+ 'different clones never collide when branches merge). Count up from the highest `C-<shard>-*` already ' +
115
+ 'present; never reuse a number, even a deleted Cell\'s. (Legacy flat `C-NNN` ids keep working as-is.)\n';
116
+ }
117
+ function block(method, root) {
118
+ return `${BEGIN}\n${header(method)}${constitutionText()}${root ? tagPoolBlock(root) : ''}${shardBlock()}\n${END}\n`;
119
+ }
120
+
121
+ function mergeInto(existing, blk) {
122
+ const i = existing.indexOf(BEGIN);
123
+ const j = existing.indexOf(END);
124
+ if (i !== -1 && j !== -1 && j > i) {
125
+ return existing.slice(0, i) + blk.trimEnd() + existing.slice(j + END.length);
126
+ }
127
+ const sep = existing.endsWith('\n') ? '\n' : '\n\n';
128
+ return existing + sep + blk; // append, preserving the user's own content
129
+ }
130
+
131
+ function harnessByKey(key) { return HARNESSES.find((h) => h.key === key); }
132
+ function resolveKeys(spec) {
133
+ if (!spec || spec === 'all') return HARNESSES.map((h) => h.key);
134
+ return String(spec).split(/[,\s]+/).map((s) => s.trim().toLowerCase()).filter(Boolean);
135
+ }
136
+
137
+ // Write/merge the constitution into the chosen harness files. Returns a report
138
+ // [{ key, label, path, action:'created'|'updated'|'appended'|'unchanged' } | { key, error }].
139
+ function writeConstitution(root, keys, method) {
140
+ const blk = block(method, root);
141
+ const report = [];
142
+ for (const key of keys) {
143
+ const h = harnessByKey(key);
144
+ if (!h) { report.push({ key, error: 'unknown harness' }); continue; }
145
+ const abs = path.join(root, h.path);
146
+ let action;
147
+ if (fs.existsSync(abs)) {
148
+ const before = fs.readFileSync(abs, 'utf8');
149
+ const after = mergeInto(before, blk);
150
+ if (after === before) action = 'unchanged';
151
+ else { fs.writeFileSync(abs, after); action = before.includes(BEGIN) ? 'updated' : 'appended'; }
152
+ } else {
153
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
154
+ fs.writeFileSync(abs, blk);
155
+ action = 'created';
156
+ }
157
+ report.push({ key, label: h.label, path: h.path, action });
158
+ }
159
+ return report;
160
+ }
161
+
162
+ module.exports = { HARNESSES, writeConstitution, resolveKeys, harnessByKey };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+ // BRANCH-EXERCISE HONESTY. During proving we count which branches the spec-derived inputs actually
3
+ // EXECUTED. A Cell can pass its `ensures` yet leave branches unexercised — and a dormant payload is,
4
+ // by definition, an unexercised branch. So instead of hiding that inside an unqualified green, we
5
+ // show the coverage boundary ("proven — 5/7 branches exercised") and feed the missed branches to
6
+ // inertness + policy. HONESTY INVARIANT preserved: coverage is a BADGE, never a Green→X downgrade by
7
+ // itself (a legitimate `throws:` guard is often unexercised on purpose); only an owner-signed policy
8
+ // (`coverage: full`) makes missing exercise gate-blocking for sensitive Cells.
9
+ //
10
+ // Instrumentation is a Babel pass: probe each branch with `__ylcov[i]++`, keyed to the ORIGINAL
11
+ // source line. Babel is an OPTIONAL dep (same as the JSX prover) — absent ⇒ we return null and there
12
+ // is simply no badge, never an error.
13
+
14
+ // Instrument the branches of `source` whose position falls within [startLine,endLine] (1-based, the
15
+ // unit's body) so each records a hit in a global `__ylcov` array. TS/JSX are lowered in the same pass
16
+ // (so the output runs), while probe line numbers stay in ORIGINAL-source coordinates. Returns
17
+ // { code, probes:[{line,kind}] } or null (Babel missing / parse fail — graceful, no badge).
18
+ function instrument(source, startLine, endLine, opts) {
19
+ opts = opts || {};
20
+ let babel, t;
21
+ try { babel = require('@babel/core'); t = require('@babel/types'); }
22
+ catch (_) { return null; }
23
+
24
+ const probes = [];
25
+ const inRange = (node) => !!node.loc && node.loc.start.line >= startLine && node.loc.start.line <= endLine;
26
+ const lineOf = (node, fallback) => ((node && node.loc) ? node.loc.start.line : fallback);
27
+ const member = (idx) => t.memberExpression(t.identifier('__ylcov'), t.numericLiteral(idx), true);
28
+ const stmtProbe = (idx) => t.expressionStatement(t.updateExpression('++', member(idx)));
29
+ const exprProbe = (idx, expr) => t.sequenceExpression([t.updateExpression('++', member(idx)), expr]);
30
+ const asBlock = (node) => t.isBlockStatement(node) ? node : t.blockStatement([t.isStatement(node) ? node : t.expressionStatement(node)]);
31
+ // add a probe as the first statement of a (possibly newly-wrapped) block; returns the block.
32
+ const probedBlock = (node, kind) => { const idx = probes.length; probes.push({ line: lineOf(node), kind }); const b = asBlock(node); b.body.unshift(stmtProbe(idx)); return b; };
33
+ const probedExpr = (node, kind) => { const idx = probes.length; probes.push({ line: lineOf(node), kind }); return exprProbe(idx, node); };
34
+
35
+ const plugin = () => ({ visitor: {
36
+ IfStatement(pth) {
37
+ const n = pth.node; if (!inRange(n)) return;
38
+ n.consequent = probedBlock(n.consequent, 'if');
39
+ if (n.alternate) n.alternate = probedBlock(n.alternate, 'else');
40
+ },
41
+ ConditionalExpression(pth) {
42
+ const n = pth.node; if (!inRange(n)) return;
43
+ n.consequent = probedExpr(n.consequent, 'ternary');
44
+ n.alternate = probedExpr(n.alternate, 'ternary');
45
+ },
46
+ LogicalExpression(pth) {
47
+ const n = pth.node; if (!inRange(n)) return;
48
+ n.right = probedExpr(n.right, 'logical'); // RHS only runs when the operator short-circuits into it
49
+ },
50
+ SwitchCase(pth) {
51
+ const n = pth.node; if (!inRange(n) || !n.consequent.length) return;
52
+ const idx = probes.length; probes.push({ line: lineOf(n), kind: 'case' });
53
+ n.consequent.unshift(stmtProbe(idx));
54
+ },
55
+ CatchClause(pth) {
56
+ const n = pth.node; if (!inRange(n)) return;
57
+ const idx = probes.length; probes.push({ line: lineOf(n), kind: 'catch' });
58
+ n.body.body.unshift(stmtProbe(idx));
59
+ },
60
+ 'ForStatement|WhileStatement|DoWhileStatement|ForInStatement|ForOfStatement'(pth) {
61
+ const n = pth.node; if (!inRange(n)) return;
62
+ n.body = probedBlock(n.body, 'loop');
63
+ },
64
+ } });
65
+
66
+ // Coverage plugin FIRST (capture branches at original positions), then TS/JSX lowering.
67
+ const plugins = [plugin];
68
+ if (opts.ts) { try { let p = require('@babel/plugin-transform-typescript'); plugins.push([p.default || p, { isTSX: !!opts.tsx, allowDeclareFields: true }]); } catch (_) { return null; } }
69
+ if (opts.jsx) { try { let p = require('@babel/plugin-transform-react-jsx'); plugins.push([p.default || p, { runtime: 'classic', pragma: '__h', pragmaFrag: '__Fragment' }]); } catch (_) { return null; } }
70
+ try {
71
+ const out = babel.transformSync(source, { filename: opts.file || 'cell.js', babelrc: false, configFile: false, compact: false, plugins });
72
+ if (!out || out.code == null) return null;
73
+ return { code: out.code, probes };
74
+ } catch (_) { return null; }
75
+ }
76
+
77
+ module.exports = { instrument };