@gonrocca/nodd 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 +21 -0
- package/README.md +350 -0
- package/extensions/nodd-agents.test.ts +129 -0
- package/extensions/nodd-agents.ts +185 -0
- package/extensions/nodd-allow.test.ts +75 -0
- package/extensions/nodd-allow.ts +76 -0
- package/extensions/nodd-enforcement.test.ts +676 -0
- package/extensions/nodd-gates.test.ts +108 -0
- package/extensions/nodd-gates.ts +121 -0
- package/extensions/nodd-kernel.test.ts +114 -0
- package/extensions/nodd-kernel.ts +593 -0
- package/extensions/nodd-models.test.ts +174 -0
- package/extensions/nodd-models.ts +253 -0
- package/extensions/nodd-promote.test.ts +150 -0
- package/extensions/nodd-promote.ts +96 -0
- package/extensions/nodd-prompt.test.ts +87 -0
- package/extensions/nodd-tools.test.ts +211 -0
- package/package.json +44 -0
- package/src/bash-classifier.test.ts +114 -0
- package/src/bash-classifier.ts +69 -0
- package/src/change-acceptance.test.ts +175 -0
- package/src/change-acceptance.ts +98 -0
- package/src/config.test.ts +61 -0
- package/src/config.ts +103 -0
- package/src/delivery.test.ts +156 -0
- package/src/delivery.ts +151 -0
- package/src/feature-doc.test.ts +120 -0
- package/src/feature-doc.ts +292 -0
- package/src/gates/authorize.test.ts +62 -0
- package/src/gates/authorize.ts +32 -0
- package/src/gates/classify.test.ts +54 -0
- package/src/gates/classify.ts +45 -0
- package/src/gates/delegate.test.ts +127 -0
- package/src/gates/delegate.ts +85 -0
- package/src/gates/evidence.test.ts +281 -0
- package/src/gates/evidence.ts +209 -0
- package/src/gates/policy.test.ts +77 -0
- package/src/gates/policy.ts +90 -0
- package/src/gates/promotion.test.ts +133 -0
- package/src/gates/promotion.ts +81 -0
- package/src/gates/registry.ts +21 -0
- package/src/gates/request.ts +41 -0
- package/src/gates/track.test.ts +80 -0
- package/src/gates/track.ts +58 -0
- package/src/io.test.ts +81 -0
- package/src/io.ts +94 -0
- package/src/ledger.test.ts +122 -0
- package/src/ledger.ts +133 -0
- package/src/manifest.test.ts +53 -0
- package/src/manifest.ts +61 -0
- package/src/models/assign.test.ts +125 -0
- package/src/models/assign.ts +138 -0
- package/src/models/picker.test.ts +141 -0
- package/src/models/picker.ts +98 -0
- package/src/models/profiles.test.ts +186 -0
- package/src/models/profiles.ts +162 -0
- package/src/models/slots.ts +48 -0
- package/src/observations.test.ts +61 -0
- package/src/observations.ts +51 -0
- package/src/odd-prose.test.ts +125 -0
- package/src/odd-prose.ts +198 -0
- package/src/outcome.test.ts +75 -0
- package/src/outcome.ts +63 -0
- package/src/promote.test.ts +129 -0
- package/src/promote.ts +64 -0
- package/src/prompt.test.ts +193 -0
- package/src/prompt.ts +136 -0
- package/src/review-candidate.test.ts +118 -0
- package/src/review-candidate.ts +81 -0
- package/src/state.test.ts +153 -0
- package/src/state.ts +163 -0
- package/test/package-invariants.test.ts +66 -0
- package/test/parity-matrix.test.ts +272 -0
- package/test/readme-contract.test.ts +182 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { GATE_IDS } from "../src/gates/registry.ts";
|
|
5
|
+
import { CANONICAL_STEPS } from "../src/manifest.ts";
|
|
6
|
+
|
|
7
|
+
const README = readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
8
|
+
|
|
9
|
+
/** A section and everything under it, up to the next heading of the same level. */
|
|
10
|
+
function section(heading: RegExp): string {
|
|
11
|
+
const lines = README.split("\n");
|
|
12
|
+
const start = lines.findIndex((line) => /^#{2,3} /.test(line) && heading.test(line));
|
|
13
|
+
assert.ok(start >= 0, `missing section matching ${heading}`);
|
|
14
|
+
const level = (/^#+/.exec(lines[start]) ?? ["##"])[0].length;
|
|
15
|
+
const end = lines.findIndex((line, i) => i > start && new RegExp(`^#{1,${level}} `).test(line));
|
|
16
|
+
return lines.slice(start, end === -1 ? lines.length : end).join("\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Every mechanism named is a mechanism that exists
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
test("every registered gate id appears in the README", () => {
|
|
23
|
+
for (const gate of GATE_IDS) {
|
|
24
|
+
assert.ok(README.includes(gate), `gate ${gate} is unmentioned`);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("every gate the README names is actually registered", () => {
|
|
29
|
+
const claimed = [...README.matchAll(/`gate-([a-z-]+)`/g)].map((m) => m[1]);
|
|
30
|
+
for (const gate of new Set(claimed)) {
|
|
31
|
+
assert.ok((GATE_IDS as readonly string[]).includes(gate), `README claims gate-${gate}, which does not exist`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("every module the README cites exists on disk", () => {
|
|
36
|
+
const cited = [...README.matchAll(/`(src\/[a-z-]+(?:\/[a-z-]+)?\.ts)`/g)].map((m) => m[1]);
|
|
37
|
+
const existing = new Set<string>();
|
|
38
|
+
for (const dir of ["src", "src/gates", "src/models"]) {
|
|
39
|
+
for (const file of readdirSync(new URL(`../${dir}/`, import.meta.url))) existing.add(`${dir}/${file}`);
|
|
40
|
+
}
|
|
41
|
+
for (const path of new Set(cited)) {
|
|
42
|
+
assert.ok(existing.has(path), `README cites ${path}, which does not exist`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// `README.includes(step)` is a substring match over the whole file, so five of
|
|
47
|
+
// the seven steps were held up by an unrelated word: `authorize` by
|
|
48
|
+
// `gate-authorize`, `classify` by `gate-classify`, `track` by `tracked`,
|
|
49
|
+
// `implement` by `implemented`, `close` by `fail-closed`. Deleting a step from
|
|
50
|
+
// the chain left the suite green. The claim is about the documented chain, so
|
|
51
|
+
// that is what this reads: scoped to its section, and by identity, not presence.
|
|
52
|
+
test("the seven canonical steps are documented, in order, in their section", () => {
|
|
53
|
+
const steps = section(/The seven canonical steps/);
|
|
54
|
+
// Taking the first match let a correct decoy above a corrupted chain pass, so
|
|
55
|
+
// the section is allowed exactly one chain and that one is the one read.
|
|
56
|
+
const chains = [...steps.matchAll(/(`[a-z-]+`(?:\s*→\s*`[a-z-]+`)+)\./g)];
|
|
57
|
+
assert.equal(chains.length, 1, "the section must hold exactly one canonical chain");
|
|
58
|
+
const named = chains[0][1].split("→").map((step) => step.trim().replaceAll("`", ""));
|
|
59
|
+
assert.deepEqual(named, [...CANONICAL_STEPS], "the documented chain is not the canonical step list");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// The honest-scope sections
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
test("the enforcement scope section exists and is substantive", () => {
|
|
66
|
+
const scope = section(/Enforcement scope/i);
|
|
67
|
+
assert.ok(scope.length > 400, "the enforcement scope must be stated, not gestured at");
|
|
68
|
+
assert.match(scope, /sub-?agent/i, "it must say what happens in sub-agents");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("the sub-agent section names the four uncovered vectors from the spike", () => {
|
|
72
|
+
const scope = section(/Enforcement scope/i);
|
|
73
|
+
assert.match(scope, /--no-extensions|extensions:/, "an agent with its own extensions list");
|
|
74
|
+
assert.match(scope, /denyExtensions|capability ceiling/i, "the capability ceiling");
|
|
75
|
+
assert.match(scope, /grandchild|depth/i, "grandchildren beyond depth 1");
|
|
76
|
+
assert.match(scope, /session state|not shared/i, "session state is not shared");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("the bash coverage section claims no exhaustiveness", () => {
|
|
80
|
+
const bash = section(/bash/i);
|
|
81
|
+
for (const overclaim of ["guarantees", "garantiza", "exhaustive", "all writes", "every possible", "cannot be bypassed"]) {
|
|
82
|
+
assert.ok(!bash.toLowerCase().includes(overclaim.toLowerCase()), `the bash section must not say "${overclaim}"`);
|
|
83
|
+
}
|
|
84
|
+
assert.match(bash, /not cover|no cubre|uncovered|bypass/i, "it must say what it misses");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("the per-process counter limitation is stated, with no claim of a session total", () => {
|
|
88
|
+
assert.match(README, /per-process|por proceso/i);
|
|
89
|
+
assert.match(README, /no aggregate|sin total|does not claim/i);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("the resume behaviour is documented as expected, with its reason", () => {
|
|
93
|
+
const resume = section(/[Rr]esum/);
|
|
94
|
+
assert.match(resume, /unverified/, "prior evidence returns as unverified");
|
|
95
|
+
assert.match(resume, /re-?run/i, "the check must be re-run");
|
|
96
|
+
assert.match(resume, /observed/i, "and the one-line reason is given");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("unverified and mismatch are distinguished", () => {
|
|
100
|
+
assert.ok(README.includes("unverified") && README.includes("mismatch"));
|
|
101
|
+
const unverifiedLine = README.split("\n").find((l) => l.includes("unverified") && l.includes("mismatch"))
|
|
102
|
+
?? section(/[Ll]edger|[Ee]vidence/);
|
|
103
|
+
assert.match(unverifiedLine, /contradic|mismatch/i, "mismatch must be described as contradicting an observation");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("the kill-switch semantics are stated without a re-enable suggestion", () => {
|
|
107
|
+
const kill = section(/kill switch|apagar|flags/i);
|
|
108
|
+
assert.match(kill, /nodd-off|enabled: false/, "the flags are named");
|
|
109
|
+
for (const forbidden = "re-enable" as const; ;) {
|
|
110
|
+
assert.ok(!/do not argue.{0,80}(then|but) re-?enable/i.test(kill), forbidden);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
assert.match(kill, /entirely|completamente|do not argue|no discut/i);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("the escalation-divergence divergence from ODD is declared, naming routing.go:68", () => {
|
|
117
|
+
assert.ok(README.includes("routing.go:68"), "the divergent clause must be cited by line");
|
|
118
|
+
const around = README.slice(Math.max(0, README.indexOf("routing.go:68") - 600), README.indexOf("routing.go:68") + 600);
|
|
119
|
+
assert.match(around, /diverg/i, "and labelled as a deliberate divergence");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// This test used to be `assert.ok(README.includes("(M)"))` per marker, which
|
|
123
|
+
// passes on any document containing those three substrings and was the stated
|
|
124
|
+
// acceptance criterion for the whole matrix. The substance -- that every (M) row
|
|
125
|
+
// names a mechanism that exists -- is checked against the code in
|
|
126
|
+
// `test/parity-matrix.test.ts`. What belongs here is only that the README's
|
|
127
|
+
// summary does not contradict the matrix it summarizes.
|
|
128
|
+
test("the README's matrix summary agrees with the matrix itself", () => {
|
|
129
|
+
const requirements = readFileSync(new URL("../.sdd/nodd/requirements.md", import.meta.url), "utf8");
|
|
130
|
+
const matrix = requirements.slice(requirements.indexOf("# ODD parity matrix"));
|
|
131
|
+
const rows = matrix.split("\n").filter((line) => /^\| \d+ \|/.test(line));
|
|
132
|
+
assert.ok(rows.length >= 50, `the matrix must be present to be summarized, parsed ${rows.length}`);
|
|
133
|
+
|
|
134
|
+
// Every class the matrix uses must be explained in the README, and the README
|
|
135
|
+
// must not advertise a class the matrix never assigns.
|
|
136
|
+
for (const marker of ["M", "P", "F"] as const) {
|
|
137
|
+
assert.ok(
|
|
138
|
+
rows.some((row) => row.includes(`**${marker}**`)),
|
|
139
|
+
`the matrix assigns no (${marker}): fix the matrix or stop advertising the class`,
|
|
140
|
+
);
|
|
141
|
+
assert.ok(README.includes(`(${marker})`), `the README must explain what (${marker}) means`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// A quoted total is how "fully classified" quietly stops being true as ODD's
|
|
145
|
+
// surface grows, so if the README states one it must be the real one.
|
|
146
|
+
const quoted = README.match(/(\d+)\s+(?:clauses|rows)\b/i);
|
|
147
|
+
if (quoted) {
|
|
148
|
+
assert.equal(Number(quoted[1]), rows.length, `the README says ${quoted[1]}, the matrix has ${rows.length}`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// What NODD does not ship
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
test("no code path references the gentle-ai binary", () => {
|
|
156
|
+
for (const dir of ["../src", "../src/gates", "../src/models", "../extensions"]) {
|
|
157
|
+
const url = new URL(`${dir}/`, import.meta.url);
|
|
158
|
+
for (const file of readdirSync(url)) {
|
|
159
|
+
if (!file.endsWith(".ts")) continue;
|
|
160
|
+
const source = readFileSync(new URL(file, url), "utf8")
|
|
161
|
+
.split("\n")
|
|
162
|
+
.filter((line) => !/^\s*(\/\/|\*|\/\*)/.test(line))
|
|
163
|
+
.join("\n");
|
|
164
|
+
assert.ok(!/gentle-ai (review|assess)/.test(source), `${dir}/${file} invokes the gentle-ai binary`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("the README does not promise a command the package does not register", () => {
|
|
170
|
+
const claimed = new Set([...README.matchAll(/`\/(nodd-[a-z-]+)/g)].map((m) => m[1]));
|
|
171
|
+
const registered = new Set<string>();
|
|
172
|
+
const url = new URL("../extensions/", import.meta.url);
|
|
173
|
+
for (const file of readdirSync(url)) {
|
|
174
|
+
if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
|
|
175
|
+
for (const match of readFileSync(new URL(file, url), "utf8").matchAll(/registerCommand\?\.\("([a-z-]+)"/g)) {
|
|
176
|
+
registered.add(match[1]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const command of claimed) {
|
|
180
|
+
assert.ok(registered.has(command), `README promises /${command}, which no extension registers`);
|
|
181
|
+
}
|
|
182
|
+
});
|