@antelopejs/dms 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.17.0
4
+
5
+ [compare changes](https://github.com/AntelopeJS/dms/compare/v0.16.2...v0.17.0)
6
+
7
+ ### 🏡 Chore
8
+
9
+ - **dms-control:** Remove seeded runs and containment journeys from verification ([#774](https://github.com/AntelopeJS/dms/pull/774))
10
+
11
+ ### ❤️ Contributors
12
+
13
+ - Antony Rizzitelli ([@Upd4ting](http://github.com/Upd4ting))
14
+
3
15
  ## v0.16.2
4
16
 
5
17
  [compare changes](https://github.com/AntelopeJS/dms/compare/v0.16.1...v0.16.2)
@@ -43,9 +43,9 @@ function baseCheckout(state, skill) {
43
43
 
44
44
  /**
45
45
  * Runs `action` while the backend is the base version's, on the run's
46
- * current database, then starts the run's own backend again. A Format
47
- * containment journey writes data with the base version inside `action`
48
- * and reads it back with the head version after it.
46
+ * current database, then starts the run's own backend again. An upgrade
47
+ * journey writes data with the base version inside `action` and reads it
48
+ * back with the head version after it.
49
49
  *
50
50
  * @param {import("./run-state.mjs").RunState} state The run
51
51
  * @param {import("./verify/skill.mjs").Skill} skill The skill, whose `prepare` builds the base
@@ -16,7 +16,6 @@ import {
16
16
  readStatus,
17
17
  setStatus,
18
18
  } from "./verify/publish.mjs";
19
- import { runSeededBreaks } from "./verify/seeded.mjs";
20
19
  import { journeysFile, loadSkill } from "./verify/skill.mjs";
21
20
  import { renderVerdict } from "./verify/verdict.mjs";
22
21
 
@@ -86,31 +85,6 @@ async function commandJourney(dir, feature, ids, values) {
86
85
  return results.every((result) => result.status === "PASS") ? 0 : 1;
87
86
  }
88
87
 
89
- /**
90
- * @param {string[]} features The features whose seeded breaks run
91
- * @param {Values} values The options of the command line
92
- * @returns {Promise<number>} The exit code
93
- */
94
- async function commandSeeded(features, values) {
95
- const skill = await skillOf(values);
96
- const outcomes = runSeededBreaks(skill, features);
97
- for (const outcome of outcomes) {
98
- print(
99
- `${outcome.caught ? "CAUGHT" : "MISSED"} ${outcome.feature} by ${outcome.caughtBy.join(", ") || "no journey"} (evidence ${outcome.run}/evidence/${outcome.feature}, control ${outcome.controlRun}/evidence/${outcome.feature})`,
100
- );
101
- if (outcome.failingInControl.length > 0)
102
- print(
103
- ` already failing without the patch, not counted: ${outcome.failingInControl.join(", ")}`,
104
- );
105
- if (outcome.harnessErrors.length > 0)
106
- print(
107
- ` failed without a failing check (harness error), not counted: ${outcome.harnessErrors.join(", ")}`,
108
- );
109
- if (!outcome.caught) print(outcome.output.replace(/^/gm, " "));
110
- }
111
- return outcomes.every((outcome) => outcome.caught) ? 0 : 1;
112
- }
113
-
114
88
  /**
115
89
  * @param {string | undefined} patch A patch file, or none for the diff
116
90
  * @param {Values} values The options of the command line
@@ -201,7 +175,7 @@ async function commandJudge(command, dir, values) {
201
175
 
202
176
  /**
203
177
  * Runs a command of the verification harness: `map`, `plan`, `journey`,
204
- * `seeded`, `doors`, `formal`, `verdict` and `publish`.
178
+ * `doors`, `formal`, `verdict` and `publish`.
205
179
  *
206
180
  * @param {string} command The command
207
181
  * @param {string[]} rest Its positional arguments
@@ -216,8 +190,6 @@ export function runVerifyCommand(command, rest, values) {
216
190
  return commandPlan(values);
217
191
  case "journey":
218
192
  return commandJourney(rest[0], rest[1], rest.slice(2), values);
219
- case "seeded":
220
- return commandSeeded(rest, values);
221
193
  case "doors":
222
194
  return commandDoors(rest[0], values);
223
195
  case "formal":
@@ -15,7 +15,6 @@ import { targetOf } from "./commands-stack.mjs";
15
15
  import { git } from "./git.mjs";
16
16
  import { resolveRenderer } from "./renderer.mjs";
17
17
  import { decodeSkillConfig } from "./verify/decode.mjs";
18
- import { upArguments } from "./verify/seeded.mjs";
19
18
  import { cliPaths, layOutCopy } from "./stack.mjs";
20
19
 
21
20
  /**
@@ -266,59 +265,3 @@ await test("a run with a renderer has no base backend", async () => {
266
265
  /no base backend/,
267
266
  );
268
267
  });
269
-
270
- /**
271
- * @param {Partial<import("./verify/skill.mjs").SkillConfig>} config What differs from a skill with a playground in its repository
272
- * @returns {import("./verify/skill.mjs").Skill} The skill
273
- */
274
- function skillWith(config) {
275
- return {
276
- dir: "/repo/.agents/skills/verify-x",
277
- name: "verify-x",
278
- repo: "/repo",
279
- config: { ...decodeSkillConfig(CONFIG, "verify.config.ts"), ...config },
280
- featuresDir: "",
281
- journeysDir: "",
282
- seededBreaksDir: "",
283
- seededDoorsDir: "",
284
- };
285
- }
286
-
287
- await test("seeded starts each run on the worktree's renderer and an external playground as it is", () => {
288
- const prepared = {
289
- worktree: "/work/seeded-auth-1",
290
- skill: "/work/seeded-auth-1/skill",
291
- };
292
- assert.deepEqual(
293
- upArguments(
294
- skillWith({ playground: "/dms/playground", renderer: "." }),
295
- prepared,
296
- ),
297
- [
298
- "up",
299
- "--playground",
300
- "/dms/playground",
301
- "--renderer",
302
- "/work/seeded-auth-1",
303
- "--skill",
304
- "/work/seeded-auth-1/skill",
305
- ],
306
- );
307
- });
308
-
309
- await test("seeded without a renderer starts the worktree's own playground", () => {
310
- const prepared = {
311
- worktree: "/work/seeded-auth-1",
312
- skill: "/work/seeded-auth-1/skill",
313
- };
314
- assert.deepEqual(
315
- upArguments(skillWith({ playground: "packages/dms/playground" }), prepared),
316
- [
317
- "up",
318
- "--playground",
319
- "/work/seeded-auth-1/packages/dms/playground",
320
- "--skill",
321
- "/work/seeded-auth-1/skill",
322
- ],
323
- );
324
- });
@@ -98,7 +98,6 @@ export const isRawJudgement = objectOf(
98
98
  location: isString,
99
99
  irreversible: isString,
100
100
  containment: isString,
101
- containmentJourney: isString,
102
101
  },
103
102
  ),
104
103
  ),
@@ -141,7 +140,6 @@ const isFormalCheck = objectOf(
141
140
  const isDoorRule = objectOf({
142
141
  globs: strings,
143
142
  funnels: strings,
144
- containments: strings,
145
143
  why: isString,
146
144
  });
147
145
 
@@ -124,7 +124,7 @@ const CONFIG = `export default {
124
124
  userFacing: ["src/**"],
125
125
  smoke: "smoke",
126
126
  smokeJourney: "onboard",
127
- doors: { data: { globs: ["src/**"], funnels: [], containments: [], why: "rows" }, format: null },
127
+ doors: { data: { globs: ["src/**"], funnels: [], why: "rows" }, format: null },
128
128
  formal: [],
129
129
  };
130
130
  `;
@@ -1,40 +1,85 @@
1
1
  import assert from "node:assert/strict";
2
- import { execFileSync } from "node:child_process";
3
- import { existsSync } from "node:fs";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import test from "node:test";
6
- import { flaggedDoors, patchFiles } from "./doors.mjs";
7
- import { loadSkill } from "./skill.mjs";
6
+ import { doorLine, flaggedDoors, patchFiles } from "./doors.mjs";
8
7
 
9
- const skill = await loadSkill(undefined, import.meta.dirname);
10
- const kinds = Object.entries(skill.config.doors)
11
- .filter(
12
- ([kind, rule]) =>
13
- rule && existsSync(join(skill.seededDoorsDir, `${kind}.patch`)),
14
- )
15
- .map(([kind]) => kind);
8
+ /** @type {import("./skill.mjs").Skill} */
9
+ const skill = {
10
+ dir: "/repo/.agents/skills/verify-x",
11
+ name: "verify-x",
12
+ repo: "/repo",
13
+ featuresDir: "",
14
+ journeysDir: "",
15
+ config: {
16
+ playground: "playground",
17
+ prepare: [],
18
+ userFacing: [],
19
+ notUserFacing: [],
20
+ smoke: "smoke",
21
+ smokeJourney: "",
22
+ formal: [],
23
+ doors: {
24
+ data: { globs: ["src/crons/**"], funnels: [], why: "deletes rows" },
25
+ outbound: {
26
+ globs: ["src/utils/*-mail.ts"],
27
+ funnels: ["src/emails.ts"],
28
+ why: "sends mail",
29
+ },
30
+ publish: null,
31
+ },
32
+ },
33
+ };
16
34
 
17
- for (const kind of kinds) {
18
- const patch = join(skill.seededDoorsDir, `${kind}.patch`);
35
+ await test("flaggedDoors names each kind a file hits, with its files, through globs and funnels", () => {
36
+ assert.deepEqual(
37
+ flaggedDoors(skill, [
38
+ "src/crons/purge.ts",
39
+ "src/emails.ts",
40
+ "src/utils/invite-mail.ts",
41
+ "src/pages/home.ts",
42
+ ]),
43
+ [
44
+ { kind: "data", files: ["src/crons/purge.ts"] },
45
+ {
46
+ kind: "outbound",
47
+ files: ["src/emails.ts", "src/utils/invite-mail.ts"],
48
+ },
49
+ ],
50
+ );
51
+ });
19
52
 
20
- await test(`the ${kind} seeded door applies to the current tree`, () => {
21
- assert.doesNotThrow(
22
- () =>
23
- execFileSync("git", ["apply", "--check", patch], {
24
- cwd: skill.repo,
25
- stdio: "pipe",
26
- }),
27
- `${kind}.patch no longer applies; refresh it against the current source`,
28
- );
29
- });
53
+ await test("flaggedDoors flags nothing for files outside every rule, a null rule included", () => {
54
+ assert.deepEqual(
55
+ flaggedDoors(skill, ["package.json", "src/pages/home.ts"]),
56
+ [],
57
+ );
58
+ });
30
59
 
31
- await test(`the ${kind} seeded door flags the ${kind} door`, () => {
32
- const flagged = flaggedDoors(skill, patchFiles(patch)).map(
33
- (door) => door.kind,
34
- );
35
- assert.ok(
36
- flagged.includes(kind),
37
- `${kind}.patch flags ${flagged.join(", ") || "nothing"}`,
38
- );
39
- });
40
- }
60
+ await test("patchFiles reads the new paths of a unified diff, not the old ones", (t) => {
61
+ const dir = mkdtempSync(join(tmpdir(), "dms-control-doors-test-"));
62
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
63
+ const patch = join(dir, "change.patch");
64
+ writeFileSync(
65
+ patch,
66
+ [
67
+ "diff --git a/src/old.ts b/src/crons/purge.ts",
68
+ "--- a/src/old.ts",
69
+ "+++ b/src/crons/purge.ts",
70
+ "@@ -1 +1 @@",
71
+ "-a",
72
+ "+b",
73
+ "",
74
+ ].join("\n"),
75
+ );
76
+ assert.deepEqual(patchFiles(patch), ["src/crons/purge.ts"]);
77
+ });
78
+
79
+ await test("doorLine lists the flagged kinds, and is empty when none is", () => {
80
+ assert.equal(
81
+ doorLine([{ kind: "data" }, { kind: "outbound" }]),
82
+ "One-way-door: data, outbound",
83
+ );
84
+ assert.equal(doorLine([]), "");
85
+ });
@@ -31,7 +31,6 @@ function parseRows(skill, raw) {
31
31
  return {
32
32
  ...row,
33
33
  kind: doorKind(skill, row.kind, `doors.rows[${index}].kind`),
34
- containmentJourney: row.containmentJourney ?? "",
35
34
  };
36
35
  });
37
36
  }
@@ -1,7 +1,6 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { join, matchesGlob } from "node:path";
3
3
  import { git } from "../git.mjs";
4
- import { flaggedDoors, patchFiles } from "./doors.mjs";
5
4
  import { loadJourneys } from "../journey.mjs";
6
5
  import { journeysFile, readFeatures } from "./skill.mjs";
7
6
 
@@ -82,20 +81,6 @@ async function featureProblems(skill, feature, tracked) {
82
81
  problems.push(`${feature.file}: docs path ${doc} does not exist`);
83
82
  }
84
83
  }
85
- const patch = join(skill.seededBreaksDir, `${feature.name}.patch`);
86
- if (!existsSync(patch)) {
87
- problems.push(
88
- `${feature.file}: seeded-breaks/${feature.name}.patch is missing`,
89
- );
90
- }
91
- const always = feature.name === skill.config.smoke;
92
- for (const file of existsSync(patch) && !always ? patchFiles(patch) : []) {
93
- if (!matches(file, feature.covers)) {
94
- problems.push(
95
- `${feature.file}: its seeded break edits ${file}, which its covers globs miss, so plan would not run it`,
96
- );
97
- }
98
- }
99
84
  return problems;
100
85
  }
101
86
 
@@ -108,11 +93,6 @@ function doorProblems(skill, tracked) {
108
93
  const problems = [];
109
94
  for (const [kind, rule] of Object.entries(skill.config.doors)) {
110
95
  if (!rule) continue;
111
- if (!existsSync(join(skill.seededDoorsDir, `${kind}.patch`))) {
112
- problems.push(
113
- `seeded-doors/${kind}.patch is missing for the ${kind} door rule`,
114
- );
115
- }
116
96
  for (const glob of [...rule.globs, ...rule.funnels]) {
117
97
  if (!tracked.some((path) => matchesGlob(path, glob))) {
118
98
  problems.push(
@@ -126,11 +106,9 @@ function doorProblems(skill, tracked) {
126
106
 
127
107
  /**
128
108
  * Checks the map against the repository: every user-facing file is
129
- * covered, every feature has journeys, each described in its file, a
130
- * seeded break that edits only files its own globs cover (the smoke
131
- * feature runs on every verdict and is exempt), existing docs
132
- * pages, every feature and door glob
133
- * matches a tracked file, and every door rule has a seeded door.
109
+ * covered, every feature has journeys, each described in its file,
110
+ * existing docs pages, and every feature and door glob matches a tracked
111
+ * file.
134
112
  *
135
113
  * @param {import("./skill.mjs").Skill} skill The skill
136
114
  * @returns {Promise<string[]>} One line per problem
@@ -159,33 +137,10 @@ export async function checkMap(skill) {
159
137
  return [...problems, ...doorProblems(skill, tracked)];
160
138
  }
161
139
 
162
- const CONTAINED_KINDS = ["data", "format"];
163
-
164
- /**
165
- * The containment journeys, as `feature/journey`, of the Data and Format
166
- * doors a set of changed files opens: a door row must name one of them,
167
- * passing in the run.
168
- *
169
- * @param {import("./skill.mjs").Skill} skill The skill
170
- * @param {string[]} changed Repository paths
171
- * @returns {string[]} The journeys, each once
172
- */
173
- function containmentJourneys(skill, changed) {
174
- const kinds = flaggedDoors(skill, changed)
175
- .map((door) => door.kind)
176
- .filter((kind) => CONTAINED_KINDS.includes(kind));
177
- return [
178
- ...new Set(
179
- kinds.flatMap((kind) => skill.config.doors[kind]?.containments ?? []),
180
- ),
181
- ];
182
- }
183
-
184
140
  /**
185
141
  * Maps the diff from the merge base of `baseRef` to HEAD onto features:
186
142
  * every touched feature plus the smoke feature runs, a user-facing file
187
- * outside every glob is an unmapped surface, and `containments` lists the
188
- * journeys that can contain each Data or Format door the diff opens.
143
+ * outside every glob is an unmapped surface.
189
144
  *
190
145
  * @param {import("./skill.mjs").Skill} skill The skill
191
146
  * @param {string} baseRef The base, such as `origin/main`
@@ -216,7 +171,6 @@ export function planFor(skill, baseRef) {
216
171
  ),
217
172
  ];
218
173
  const smoke = skill.config.smoke;
219
- const containments = containmentJourneys(skill, changed);
220
174
  return {
221
175
  baseRef,
222
176
  baseSha,
@@ -225,7 +179,6 @@ export function planFor(skill, baseRef) {
225
179
  touched,
226
180
  unmapped,
227
181
  docs,
228
- containments,
229
182
  run: [smoke, ...[...touchedNames].filter((name) => name !== smoke)],
230
183
  };
231
184
  }
@@ -10,8 +10,8 @@ const CONFIG_FILE = "verify.config.ts";
10
10
  const OUTPUT_LIMIT = 64 * 1024 * 1024;
11
11
 
12
12
  /**
13
- * @typedef {{ dir: string, name: string, repo: string, config: SkillConfig, featuresDir: string, journeysDir: string, seededBreaksDir: string, seededDoorsDir: string }} Skill
14
- * @typedef {{ playground: string, renderer?: string, prepare: string[][], userFacing: string[], notUserFacing: string[], smoke: string, smokeJourney: string, doors: Record<string, { globs: string[], funnels: string[], containments: string[], why: string } | null>, formal: import("./types.mjs").FormalCheck[] }} SkillConfig
13
+ * @typedef {{ dir: string, name: string, repo: string, config: SkillConfig, featuresDir: string, journeysDir: string }} Skill
14
+ * @typedef {{ playground: string, renderer?: string, prepare: string[][], userFacing: string[], notUserFacing: string[], smoke: string, smokeJourney: string, doors: Record<string, { globs: string[], funnels: string[], why: string } | null>, formal: import("./types.mjs").FormalCheck[] }} SkillConfig
15
15
  */
16
16
 
17
17
  /**
@@ -60,8 +60,8 @@ function discover(cwd) {
60
60
 
61
61
  /**
62
62
  * Loads a verification skill: its `verify.config.ts` (imported with Node's
63
- * TypeScript type stripping), its feature files and where its journeys,
64
- * seeded breaks and seeded doors live. Without `--skill`, the one
63
+ * TypeScript type stripping), its feature files and where its journeys
64
+ * live. Without `--skill`, the one
65
65
  * `.agents/skills/verify-*` folder of the checkout holding `cwd`.
66
66
  *
67
67
  * @param {string | undefined} option The `--skill` folder
@@ -86,8 +86,6 @@ export async function loadSkill(option, cwd) {
86
86
  config,
87
87
  featuresDir: join(dir, "features"),
88
88
  journeysDir: join(dir, "journeys"),
89
- seededBreaksDir: join(dir, "seeded-breaks"),
90
- seededDoorsDir: join(dir, "seeded-doors"),
91
89
  };
92
90
  }
93
91
 
@@ -10,12 +10,12 @@
10
10
  * @typedef {{ exitCode: number | null, violated: string[], passing: string[] }} Rules
11
11
  * @typedef {{ name: string, exitCode: number | null, violated: string[], passing: string[], folder: string, commits?: Commits, behaviour?: BehaviourChange, checkChanged?: boolean, base?: Rules | null, fixed?: string[], missing?: string[] }} FormalResult
12
12
  * @typedef {{ feature: string, files: string[] }} TouchedFeature
13
- * @typedef {{ baseRef: string, baseSha: string, headSha: string, changed: string[], touched: TouchedFeature[], unmapped: string[], docs: string[], containments: string[], run: string[] }} Plan
14
- * @typedef {{ kind?: string, location?: string, irreversible?: string, containment?: string, containmentJourney?: string }} RawDoorRow
13
+ * @typedef {{ baseRef: string, baseSha: string, headSha: string, changed: string[], touched: TouchedFeature[], unmapped: string[], docs: string[], run: string[] }} Plan
14
+ * @typedef {{ kind?: string, location?: string, irreversible?: string, containment?: string }} RawDoorRow
15
15
  * @typedef {{ kind?: string, location?: string, reason?: string }} RawFalsePositive
16
16
  * @typedef {{ page: string, matches?: boolean, note?: string }} RawDocsJudgement
17
17
  * @typedef {{ verifier?: string, notVerified?: string[], attention?: string[], docs?: RawDocsJudgement[], doors?: { none?: string, rows?: RawDoorRow[], falsePositives?: RawFalsePositive[] } }} RawJudgement
18
- * @typedef {{ kind: string, location: string, irreversible: string, containment: string, containmentJourney: string }} DoorRow
18
+ * @typedef {{ kind: string, location: string, irreversible: string, containment: string }} DoorRow
19
19
  * @typedef {{ kind: string, location: string, reason: string }} FalsePositive
20
20
  * @typedef {{ page: string, matches: boolean, note: string }} DocsJudgement
21
21
  * @typedef {{ notVerified: string[], attention: string[], docs: DocsJudgement[], doorsNone: string | null, doors: DoorRow[], falsePositives: FalsePositive[], verifier: string }} Judgement
@@ -23,7 +23,6 @@
23
23
  * @typedef {{ feature: string, journey: string, title: string, entryPoint: string, status: "PASS" | "FAIL", database: string, steps: { name: string, checks: JourneyCheck[] }[], error: string | null, folder: string }} FeatureResult
24
24
  * @typedef {{ result: string, headSha: string, skillSha: string, pr: number, reasons: string[] }} VerdictCore
25
25
  * @typedef {VerdictCore & { markdown: string }} Verdict
26
- * @typedef {{ feature: string, caught: boolean, run: string, controlRun: string, caughtBy: string[], failingInControl: string[], harnessErrors: string[], output: string }} SeededOutcome
27
26
  */
28
27
 
29
28
  export {};
@@ -12,8 +12,6 @@ import {
12
12
  import { planFor } from "./map.mjs";
13
13
  import { journeysFile } from "./skill.mjs";
14
14
 
15
- const CONTAINED_KINDS = new Set(["data", "format"]);
16
-
17
15
  /**
18
16
  * The GitHub `owner/repo` of the checkout's origin remote.
19
17
  *
@@ -58,15 +56,13 @@ function readResults(state, feature) {
58
56
  * @param {import("../run-state.mjs").RunState} state The run
59
57
  * @param {import("./types.mjs").Plan} plan The plan
60
58
  * @param {(path: string) => string} link The evidence link of a path
61
- * @returns {Promise<{ rows: string[], reasons: string[], byJourney: Map<string, import("./types.mjs").FeatureResult> }>} The table rows and why the verdict fails
59
+ * @returns {Promise<{ rows: string[], reasons: string[] }>} The table rows and why the verdict fails
62
60
  */
63
61
  async function journeyRows(skill, state, plan, link) {
64
62
  /** @type {string[]} */
65
63
  const rows = [];
66
64
  /** @type {string[]} */
67
65
  const reasons = [];
68
- /** @type {Map<string, import("./types.mjs").FeatureResult>} */
69
- const byJourney = new Map();
70
66
  for (const feature of plan.run) {
71
67
  const results = readResults(state, feature);
72
68
  const file = journeysFile(skill, feature);
@@ -85,56 +81,24 @@ async function journeyRows(skill, state, plan, link) {
85
81
  );
86
82
  if (result.status === "FAIL")
87
83
  reasons.push(`${feature}/${result.journey} failed`);
88
- byJourney.set(`${feature}/${result.journey}`, result);
89
84
  const outcome = result.status === "PASS" ? "pass" : "**fail**";
90
85
  rows.push(
91
86
  `| ${feature} | \`${result.journey}\`: ${result.entryPoint} | ${outcome}: ${result.title} | [${result.folder}](${link(result.folder)}) |`,
92
87
  );
93
88
  }
94
89
  }
95
- return { rows, reasons, byJourney };
96
- }
97
-
98
- /**
99
- * @param {import("../run-state.mjs").RunState} state The run
100
- * @param {Map<string, import("./types.mjs").FeatureResult>} byJourney The results of this verdict's journeys
101
- * @param {string} id The containment journey, as `feature/journey`
102
- * @returns {import("./types.mjs").FeatureResult | undefined} Its result
103
- */
104
- function containmentResult(state, byJourney, id) {
105
- const [feature, journey] = id.split("/");
106
- return (
107
- byJourney.get(id) ??
108
- readResults(state, feature).find((result) => result.journey === journey)
109
- );
90
+ return { rows, reasons };
110
91
  }
111
92
 
112
93
  /**
113
94
  * @param {import("./skill.mjs").Skill} skill The skill
114
- * @param {import("../run-state.mjs").RunState} state The run
115
95
  * @param {import("./types.mjs").Judgement} judgement The verifier's judgement
116
- * @param {Map<string, import("./types.mjs").FeatureResult>} byJourney The results of this verdict's journeys
117
96
  * @param {import("./types.mjs").Plan} plan The plan
118
97
  * @returns {string[]} Why the doors fail the verdict
119
98
  */
120
- function doorReasons(skill, state, judgement, byJourney, plan) {
99
+ function doorReasons(skill, judgement, plan) {
121
100
  /** @type {string[]} */
122
101
  const reasons = [];
123
- for (const row of judgement.doors.filter((door) =>
124
- CONTAINED_KINDS.has(door.kind),
125
- )) {
126
- const allowed = skill.config.doors[row.kind]?.containments ?? [];
127
- const journey = containmentResult(state, byJourney, row.containmentJourney);
128
- if (!allowed.includes(row.containmentJourney)) {
129
- reasons.push(
130
- `${row.kind} door at ${row.location} names ${row.containmentJourney || "no journey"}; its containment must be one of ${allowed.join(", ")}`,
131
- );
132
- } else if (journey?.status !== "PASS") {
133
- reasons.push(
134
- `${row.kind} door at ${row.location}: containment journey ${row.containmentJourney} did not pass in this run`,
135
- );
136
- }
137
- }
138
102
  for (const door of flaggedDoors(skill, plan.changed)) {
139
103
  for (const file of door.files) {
140
104
  const listed = judgement.doors.some(
@@ -168,12 +132,10 @@ function doorsSection(judgement) {
168
132
  ...dismissed,
169
133
  "| Kind | Location | What cannot be undone | Containment |",
170
134
  "|---|---|---|---|",
171
- ...judgement.doors.map((door) => {
172
- const journey = door.containmentJourney
173
- ? ` (journey \`${door.containmentJourney}\`)`
174
- : "";
175
- return `| ${door.kind} | \`${door.location}\` | ${door.irreversible} | ${door.containment}${journey} |`;
176
- }),
135
+ ...judgement.doors.map(
136
+ (door) =>
137
+ `| ${door.kind} | \`${door.location}\` | ${door.irreversible} | ${door.containment} |`,
138
+ ),
177
139
  ];
178
140
  }
179
141
 
@@ -266,11 +228,6 @@ function markdown(skill, verdict, parts) {
266
228
  "### Touched features",
267
229
  ...bullets(touched, "none"),
268
230
  `- ${skill.config.smoke}: ${skill.config.smokeJourney}`,
269
- ...(plan.containments.length > 0
270
- ? [
271
- `- containment journeys of the Data and Format doors: ${plan.containments.map((feature) => `\`${feature}\``).join(", ")}`,
272
- ]
273
- : []),
274
231
  "",
275
232
  ...(plan.unmapped.length > 0
276
233
  ? [
@@ -344,7 +301,7 @@ export async function renderVerdict(skill, state, pr, judgement) {
344
301
  ...journeys.reasons,
345
302
  ...plan.unmapped.map((file) => `unmapped surface ${file}`),
346
303
  ...docsReasons(judgement, plan),
347
- ...doorReasons(skill, state, judgement, journeys.byJourney, plan),
304
+ ...doorReasons(skill, judgement, plan),
348
305
  ...formal.reasons,
349
306
  );
350
307
  const attention = judgement.attention;
@@ -28,7 +28,6 @@ Verify a change (the .agents/skills/verify-* folder of the checkout, or --skill
28
28
  map check the feature map covers every user-facing file
29
29
  plan [--base <ref>] map the diff to features; list unmapped surfaces
30
30
  journey <run> <feature> [ids...] run a feature's journeys and record evidence
31
- seeded <feature...> run each feature unpatched and with its seeded break
32
31
  doors [patch] [--base <ref>] print the One-way-door line for a patch, or for the diff
33
32
  formal <run> [--all] run the formal checks that cover the diff (or every one)
34
33
  verdict <run> --pr <n> render evidence/verdict.md from the results and evidence/judgement.json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms",
3
- "version": "0.16.2",
3
+ "version": "0.17.0",
4
4
  "description": "AntelopeJS DMS. Plug in your PostgreSQL, MongoDB or RethinkDB database and your coding agent builds a back office and analytics dashboard as typed TypeScript. Real-time updates on the data the DMS manages.",
5
5
  "keywords": [
6
6
  "antelopejs",
@@ -1,217 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import {
3
- cpSync,
4
- existsSync,
5
- mkdtempSync,
6
- rmSync,
7
- writeFileSync,
8
- } from "node:fs";
9
- import { tmpdir } from "node:os";
10
- import { join, relative, resolve } from "node:path";
11
- import { git } from "../git.mjs";
12
- import { CONTROL_BIN } from "../stack.mjs";
13
- import { readFeatureResults } from "./decode.mjs";
14
- import { runPrepare } from "./skill.mjs";
15
-
16
- const OUTPUT_LIMIT = 64 * 1024 * 1024;
17
-
18
- /**
19
- * @param {string} cwd Where to run it
20
- * @param {string} program The program
21
- * @param {string[]} args Its arguments
22
- * @returns {import("node:child_process").SpawnSyncReturns<string>} The run
23
- */
24
- function run(cwd, program, args) {
25
- return spawnSync(program, args, {
26
- cwd,
27
- encoding: "utf8",
28
- env: { ...process.env, CI: "1" },
29
- maxBuffer: OUTPUT_LIMIT,
30
- });
31
- }
32
-
33
- /**
34
- * @param {string} cwd Where to run it
35
- * @param {string} program The program
36
- * @param {string[]} args Its arguments
37
- * @returns {string} Its trimmed output
38
- */
39
- function mustRun(cwd, program, args) {
40
- const result = run(cwd, program, args);
41
- if (result.status !== 0) {
42
- throw new Error(
43
- `${program} ${args.join(" ")} failed in ${cwd}:\n${result.stdout}${result.stderr}`,
44
- );
45
- }
46
- return result.stdout.trim();
47
- }
48
-
49
- /**
50
- * A scratch worktree of HEAD with the current skill copied in, the patch
51
- * applied when there is one, and the config's `prepare` commands run. The
52
- * worktree is the skill's repository, which is also the renderer checkout
53
- * when the config names a `renderer`: the patch changes what the run serves.
54
- *
55
- * @param {import("./skill.mjs").Skill} skill The skill
56
- * @param {string} label What the worktree is for
57
- * @param {string | null} patch The patch file, or `null` for the control
58
- * @returns {{ worktree: string, skill: string }} The worktree and its skill copy
59
- */
60
- function prepareWorktree(skill, label, patch) {
61
- const worktree = mkdtempSync(
62
- join(tmpdir(), `${skill.name}-seeded-${label}-`),
63
- );
64
- git(["worktree", "add", "--detach", "--force", worktree, "HEAD"], skill.repo);
65
- const copy = join(worktree, relative(skill.repo, skill.dir));
66
- rmSync(copy, { recursive: true, force: true });
67
- cpSync(skill.dir, copy, { recursive: true });
68
- if (patch) mustRun(worktree, "git", ["apply", patch]);
69
- runPrepare(skill, worktree);
70
- return { worktree, skill: copy };
71
- }
72
-
73
- /**
74
- * @param {import("./skill.mjs").Skill} skill The skill
75
- * @param {string} worktree The scratch worktree
76
- */
77
- function removeWorktree(skill, worktree) {
78
- git(["worktree", "remove", "--force", worktree], skill.repo);
79
- }
80
-
81
- /**
82
- * The `up` arguments for a scratch worktree: the config's playground and
83
- * renderer, resolved in the worktree unless absolute.
84
- *
85
- * @param {import("./skill.mjs").Skill} skill The skill
86
- * @param {{ worktree: string, skill: string }} prepared The scratch worktree and its skill copy
87
- * @returns {string[]} The arguments
88
- */
89
- export function upArguments(skill, prepared) {
90
- const { playground, renderer } = skill.config;
91
- return [
92
- "up",
93
- "--playground",
94
- resolve(prepared.worktree, playground),
95
- ...(renderer ? ["--renderer", resolve(prepared.worktree, renderer)] : []),
96
- "--skill",
97
- prepared.skill,
98
- ];
99
- }
100
-
101
- /**
102
- * @param {{ worktree: string, skill: string }} prepared The scratch worktree and its skill copy
103
- * @param {import("./skill.mjs").Skill} skill The skill
104
- * @param {string} feature The feature
105
- * @returns {{ run: string, results: import("./types.mjs").FeatureResult[], output: string }} The run folder, its results and output
106
- */
107
- function runFeatureIn(prepared, skill, feature) {
108
- /**
109
- * @param {string[]} args The dms-control arguments
110
- * @returns {import("node:child_process").SpawnSyncReturns<string>} The run
111
- */
112
- const control = (args) =>
113
- run(prepared.worktree, process.execPath, [CONTROL_BIN, ...args]);
114
- const started = control(upArguments(skill, prepared));
115
- const dir = started.stdout.trim().split("\n").at(-1) ?? "";
116
- if (started.status !== 0 || !existsSync(join(dir, "run.json"))) {
117
- return {
118
- run: dir,
119
- results: [],
120
- output: `up failed:\n${started.stdout}${started.stderr}`,
121
- };
122
- }
123
- const journeys = control([
124
- "journey",
125
- dir,
126
- feature,
127
- "--skill",
128
- prepared.skill,
129
- ]);
130
- control(["down", dir]);
131
- const file = join(dir, "evidence", feature, "feature.json");
132
- const results = readFeatureResults(file);
133
- return { run: dir, results, output: `${journeys.stdout}${journeys.stderr}` };
134
- }
135
-
136
- /**
137
- * @param {import("./types.mjs").FeatureResult} result A journey result
138
- * @returns {boolean} Whether one of its checks failed
139
- */
140
- function failsACheck(result) {
141
- return result.steps.some((step) => step.checks.some((check) => !check.pass));
142
- }
143
-
144
- /**
145
- * @param {string} feature The feature
146
- * @param {ReturnType<typeof runFeatureIn>} control The unpatched run
147
- * @param {ReturnType<typeof runFeatureIn>} seeded The patched run
148
- * @returns {import("./types.mjs").SeededOutcome} The outcome
149
- */
150
- function judge(feature, control, seeded) {
151
- const passedInControl = new Set(
152
- control.results
153
- .filter((result) => result.status === "PASS")
154
- .map((result) => result.journey),
155
- );
156
- const failed = seeded.results.filter((result) => result.status === "FAIL");
157
- const caughtBy = failed
158
- .filter(
159
- (result) => passedInControl.has(result.journey) && failsACheck(result),
160
- )
161
- .map((result) => result.journey);
162
- return {
163
- feature,
164
- caught: caughtBy.length > 0,
165
- run: seeded.run,
166
- controlRun: control.run,
167
- caughtBy,
168
- failingInControl: control.results
169
- .filter((result) => result.status === "FAIL")
170
- .map((result) => result.journey),
171
- harnessErrors: failed
172
- .filter((result) => !failsACheck(result))
173
- .map((result) => result.journey),
174
- output: seeded.output,
175
- };
176
- }
177
-
178
- /**
179
- * Runs each feature's journeys twice from scratch worktrees of HEAD: once
180
- * unpatched as the control, once with `seeded-breaks/<feature>.patch`. A
181
- * break is caught only when a journey that passes in the control fails one
182
- * of its checks with the patch; a failure without a failing check is a
183
- * harness error and does not count. Worktrees are removed; both runs keep
184
- * their evidence, and `seeded.json` in the patched run records the outcome.
185
- *
186
- * @param {import("./skill.mjs").Skill} skill The skill
187
- * @param {string[]} features The features
188
- * @returns {import("./types.mjs").SeededOutcome[]} One outcome per feature
189
- */
190
- export function runSeededBreaks(skill, features) {
191
- const control = prepareWorktree(skill, "control", null);
192
- try {
193
- return features.map((feature) => {
194
- const unpatched = runFeatureIn(control, skill, feature);
195
- const patch = join(skill.seededBreaksDir, `${feature}.patch`);
196
- const seeded = prepareWorktree(skill, feature, patch);
197
- try {
198
- const outcome = judge(
199
- feature,
200
- unpatched,
201
- runFeatureIn(seeded, skill, feature),
202
- );
203
- if (existsSync(outcome.run)) {
204
- writeFileSync(
205
- join(outcome.run, "evidence", "seeded.json"),
206
- `${JSON.stringify(outcome, null, 2)}\n`,
207
- );
208
- }
209
- return outcome;
210
- } finally {
211
- removeWorktree(skill, seeded.worktree);
212
- }
213
- });
214
- } finally {
215
- removeWorktree(skill, control.worktree);
216
- }
217
- }