@arjunkhera/atlas 0.2.3 → 0.3.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.
@@ -1,130 +0,0 @@
1
- // `atlas test onboarding`: the answer key, and the fixed script that compares.
2
- //
3
- // Milestone M2, slice L2 stage one, done line 5: "The fresh-agent test scores
4
- // 6 of 6 under the isolation rule."
5
- //
6
- // Why this exists. The M2 criterion said a fresh agent "completes the six-step
7
- // task list from the files alone, scored 6 of 6 by the answer key", and no
8
- // slice built the key or the scorer. Six of six was therefore a self-grade
9
- // (panel finding P4). This is the key, and this is the scorer.
10
- //
11
- // The scorer runs OUTSIDE the isolated session. The isolated session has the
12
- // Atlas package off, so it cannot score itself even if it wanted to. It writes
13
- // its six answers to a file; this reads that file.
14
- import { readFileSync, existsSync } from 'node:fs';
15
- import { resolve } from 'node:path';
16
- import { parseYaml, secretPatterns } from '../../shape/check.mjs';
17
-
18
-
19
- // The isolation rule, in the words of closure R3. The scorer prints it, so the
20
- // person who runs the test sets the session up the same way every time.
21
- export const ISOLATION = Object.freeze([
22
- 'no memory directory',
23
- 'no global instructions',
24
- 'the Engram MCP disconnected',
25
- 'the Atlas package off',
26
- 'the repo at HEAD',
27
- ]);
28
-
29
- // The answer key is read from a path. It never ships in the package and never
30
- // sits in the repository under test, because the agent under test must not be
31
- // able to read it (amendment A1.9, the L2 stage one clause).
32
- export function loadKey(path) {
33
- if (!path || path === true) throw new Error('give the answer key with --key <path>. Keys stay outside the package and outside the repo under test.');
34
- const full = resolve(String(path));
35
- if (!existsSync(full)) throw new Error(`there is no answer key at ${full}`);
36
- const key = parseYaml(readFileSync(full, 'utf8'));
37
- if (!Array.isArray(key?.tasks) || !key.tasks.length) throw new Error(`${full} carries no tasks`);
38
- return { ...key, path: full };
39
- }
40
-
41
- // The task list as the isolated session sees it: the questions, and nothing
42
- // else. No sources, no expected strings.
43
- export function questions(key) {
44
- return key.tasks.map((task) => ({ id: task.id, number: task.number, ask: task.ask }));
45
- }
46
-
47
- const normalise = (text) => String(text ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
48
-
49
- // A phrase must appear as a whole word, not inside another one. Plain substring
50
- // matching passed "the dependencies file is nonexistent" for the phrase "none",
51
- // because "none" sits inside "nonexistent" (acceptance review of batch 16,
52
- // finding S2-5).
53
- //
54
- // What this still cannot do: it compares text, not meaning. "do not run
55
- // scripts/stack.mjs" carries the right words in the wrong order and passes.
56
- // The frozen text asks for "a fixed script that compares", so that limit is the
57
- // design, and it is written down here rather than left to be discovered.
58
- const says = (said, phrase) => {
59
- const wanted = normalise(phrase);
60
- if (!wanted) return false;
61
- // The boundary is a letter, a digit or an underscore. Dots, slashes, commas
62
- // and hyphens are boundaries, so "atlas/deploy.yaml." at the end of a
63
- // sentence still matches, and "none" inside "nonexistent" still does not.
64
- const edge = '[^a-z0-9_]';
65
- return new RegExp(`(?:^|${edge})${wanted.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:$|${edge})`).test(said);
66
- };
67
-
68
- // One answer against one task.
69
- export function scoreTask(task, answer, patterns) {
70
- const said = normalise(answer);
71
- if (!said) return { id: task.id, number: task.number, pass: false, source: task.source, missing: task.must_say ?? [], why: 'no answer was given' };
72
-
73
- // An answer that carries a credential value fails, whatever else it says.
74
- // The file it read holds names only, so a value can only have come from
75
- // somewhere it should not have been read from.
76
- for (const pattern of patterns) {
77
- if (pattern.test(String(answer ?? ''))) {
78
- return { id: task.id, number: task.number, pass: false, source: task.source, missing: [], why: 'the answer holds something that looks like a credential value; these files carry names only' };
79
- }
80
- }
81
-
82
- // The answer must cite a file. The test is whether the seven files answer on
83
- // their own, so an answer that names no file proves nothing about them
84
- // (acceptance review of batch 16, finding S2-5).
85
- const cites = (task.cites ?? [task.source]).some((path) => says(said, path));
86
- if (!cites) {
87
- return { id: task.id, number: task.number, pass: false, source: task.source, missing: [task.source], why: `the answer names no source file; it should name ${task.source}` };
88
- }
89
-
90
- const anyOf = task.accept_any_of ?? [];
91
- if (anyOf.length && anyOf.some((phrase) => says(said, phrase))) {
92
- return { id: task.id, number: task.number, pass: true, source: task.source, missing: [], why: `the answer says what ${task.source} says, and names it` };
93
- }
94
- const missing = (task.must_say ?? []).filter((phrase) => !says(said, phrase));
95
- return {
96
- id: task.id,
97
- number: task.number,
98
- pass: !missing.length,
99
- source: task.source,
100
- missing,
101
- why: missing.length ? `the answer does not name ${missing.join(', ')}; the file that says it is ${task.source}` : `the answer names what ${task.source} says, and names the file`,
102
- };
103
- }
104
-
105
- // The whole run. `answers` is a map of task id to the answer text the isolated
106
- // session wrote.
107
- export function score(key, answers, { patterns = secretPatterns() } = {}) {
108
- const rows = key.tasks.map((task) => scoreTask(task, answers?.[task.id] ?? answers?.[String(task.number)] ?? null, patterns));
109
- const passed = rows.filter((row) => row.pass).length;
110
- return {
111
- repo: key.repo,
112
- key: key.path,
113
- isolation: ISOLATION,
114
- rows,
115
- passed,
116
- of: key.tasks.length,
117
- pass: passed >= (key.pass ?? key.tasks.length),
118
- };
119
- }
120
-
121
- // The answers file the isolated session writes. YAML or JSON, one entry per
122
- // task id. Anything the key does not name is ignored, and a task with no entry
123
- // scores zero.
124
- export function readAnswers(path) {
125
- const text = readFileSync(path, 'utf8');
126
- const data = text.trimStart().startsWith('{') ? JSON.parse(text) : parseYaml(text);
127
- const rows = data?.answers ?? data;
128
- if (!rows || typeof rows !== 'object' || Array.isArray(rows)) throw new Error(`${path} is not a map of task id to answer`);
129
- return rows;
130
- }