@agentsfleet/orly 0.10.5 → 0.10.7
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/AGENTS.md +6 -6
- package/README.md +26 -8
- package/audits/agents-md.md +7 -3
- package/audits/doc-read.sh +97 -16
- package/audits/spec-template.sh +30 -113
- package/audits/spec-template.ts +193 -0
- package/core/operating-model.md +6 -6
- package/dispatch/lifecycle.md +98 -42
- package/dispatch/verify.md +30 -48
- package/dispatch/write_spec.md +56 -82
- package/docs/ORLY_ARCHITECTURE.md +13 -3
- package/docs/RULE_ENFORCEMENT.md +2 -2
- package/docs/TEMPLATE.md +82 -77
- package/docs/VERIFY_TIERS.md +20 -26
- package/llms.txt +17 -12
- package/package.json +1 -1
- package/registry.json +4 -0
- package/skills/orly-spec-new/SKILL.md +3 -1
- package/skills/orly-write-integration-test/SKILL.md +1 -1
- package/skills/orly-write-unit-test/SKILL.md +1 -1
- package/src/cli.ts +14 -16
- package/src/criteria.test.ts +47 -6
- package/src/criteria.ts +30 -49
- package/src/criteria_spec.ts +53 -6
- package/src/criteria_support.ts +5 -4
- package/src/gates.test.ts +2 -2
- package/src/gates_spec.test.ts +40 -1
- package/src/gates_test_support.ts +18 -4
- package/src/install.ts +16 -81
- package/src/render.test.ts +5 -7
- package/src/setup.test.ts +116 -0
- package/src/spec_baseline.test.ts +96 -0
- package/src/spec_template.test.ts +109 -0
- package/src/spec_template_io.test.ts +97 -0
- package/src/validation.ts +9 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { validateSpec } from "../audits/spec-template";
|
|
4
|
+
import { specFixture } from "./gates_test_support";
|
|
5
|
+
|
|
6
|
+
const COMMANDS = { conform: [["true"]], "verify.unit": [["true"]] };
|
|
7
|
+
const VALID = specFixture();
|
|
8
|
+
const MAPPING = "| 1.1 | unit | `fixture_test` | Valid input is accepted. |";
|
|
9
|
+
const REFERENCE = "1. `README.md` — fixture reference.";
|
|
10
|
+
const RUBRIC_ROW = "| S1 | Declared checks | `true` | exit 0 | P0 | |";
|
|
11
|
+
const EXISTS = () => true;
|
|
12
|
+
|
|
13
|
+
describe("spec content readiness", () => {
|
|
14
|
+
test("a filled spec passes without product-specific commands or paths", () => {
|
|
15
|
+
expect(validateSpec(VALID, COMMANDS, EXISTS)).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test.each([
|
|
19
|
+
["empty section", "Fixture content for Overview.", "", "empty required section"],
|
|
20
|
+
["missing reference", REFERENCE, "No references.", "read-first section needs"],
|
|
21
|
+
["absolute reference", REFERENCE, "1. `/etc/hosts` — outside the repository.", "repository file"],
|
|
22
|
+
["parent reference", REFERENCE, "1. `../other.md` — outside.", "repository file"],
|
|
23
|
+
["unresolved reference", REFERENCE, "1. `docs/{name}.md` — incomplete.", "repository file"],
|
|
24
|
+
["missing test", MAPPING, "", "no matching tiered test"],
|
|
25
|
+
["wrong test", MAPPING, MAPPING.replace("fixture_test", "other_test"), "no matching tiered test"],
|
|
26
|
+
["missing assertion", MAPPING, MAPPING.replace("Valid input is accepted.", ""), "no matching tiered test"],
|
|
27
|
+
["missing tier", MAPPING, MAPPING.replace("unit", ""), "no matching tiered test"],
|
|
28
|
+
["orphan test", MAPPING, MAPPING.replace("1.1", "2.1"), "unknown Dimension"],
|
|
29
|
+
["missing columns", "| Dimension | Tier | Test | Asserts |", "| Wrong | Tier | Test | Asserts |", "needs Dimension"],
|
|
30
|
+
["missing proof name", "→ Test `fixture_test`", "→ decide later", "every Dimension"],
|
|
31
|
+
["command suffix", RUBRIC_ROW, RUBRIC_ROW.replace("`true`", "`true-other`"), "misses declared command"],
|
|
32
|
+
["missing expected", RUBRIC_ROW, RUBRIC_ROW.replace("exit 0", ""), "misses declared command"],
|
|
33
|
+
["missing command column", "| # | Criterion | Verify | Expected | Priority | Graded |", "| # | Criterion | Wrong | Expected | Priority | Graded |", "needs Verify"],
|
|
34
|
+
])("rejects %s", (_name, old, replacement, finding) => {
|
|
35
|
+
expect(validateSpec(VALID.replace(old, replacement), COMMANDS, EXISTS).join("\n")).toContain(finding);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test.each(["{{fill:goal}}", "{unresolved_goal}", "<!-- tpl: delete me -->", "path/to/file.ext"])("rejects authoring residue %s", (slot) => {
|
|
39
|
+
expect(validateSpec(`${VALID}\n${slot}`, COMMANDS, EXISTS)).toContain("unfilled template placeholder or authoring guidance");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("comments and fenced headings cannot supply required sections", () => {
|
|
43
|
+
const removed = VALID.replace("## Overview\n\nFixture content for Overview.", "");
|
|
44
|
+
const text = `${removed}\n<!--\n## Overview\nHidden.\n-->\n\n\`\`\`\n## Overview\nFenced.\n\`\`\``;
|
|
45
|
+
expect(validateSpec(text, COMMANDS, EXISTS).join("\n")).toContain("empty required section: ^Overview");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test.each(["````", "~~~"])("a %s code fence cannot supply a required heading", (fence) => {
|
|
49
|
+
const removed = VALID.replace("## Overview\n\nFixture content for Overview.", "");
|
|
50
|
+
const text = `${removed}\n${fence}\n\`\`\`\n## Overview\nStill inside code.\n${fence}\n`;
|
|
51
|
+
expect(validateSpec(text, COMMANDS, EXISTS).join("\n")).toContain("empty required section: ^Overview");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("a quoted heading cannot supply a required section", () => {
|
|
55
|
+
const removed = VALID.replace("## Overview\n\nFixture content for Overview.", "");
|
|
56
|
+
expect(validateSpec(`${removed}\n> ## Overview\n> Quoted example.\n`, COMMANDS, EXISTS).join("\n")).toContain("empty required section: ^Overview");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("dimensions inside code do not count as implementation requirements", () => {
|
|
60
|
+
const dimension = "- **Dimension 1.1** — DONE — fixture behaviour → Test `fixture_test`";
|
|
61
|
+
const text = VALID.replace(dimension, `\`\`\`\n${dimension}\n\`\`\``);
|
|
62
|
+
expect(validateSpec(text, COMMANDS, EXISTS).join("\n")).toContain("every Dimension");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("comments containing a command do not satisfy its rubric row", () => {
|
|
66
|
+
const text = VALID.replace(RUBRIC_ROW, "<!-- true -->");
|
|
67
|
+
expect(validateSpec(text, COMMANDS, EXISTS).join("\n")).toContain("misses declared command verbatim: true");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("missing files fail while URLs and file fragments resolve", () => {
|
|
71
|
+
expect(validateSpec(VALID, COMMANDS, () => false).join("\n")).toContain("pointer does not resolve");
|
|
72
|
+
const text = VALID.replace(REFERENCE, "1. https://example.com/reference — upstream.\n2. `README.md#example` — local.");
|
|
73
|
+
expect(validateSpec(text, COMMANDS, (path) => path === "README.md")).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("duplicate dimensions fail even when they share a test", () => {
|
|
77
|
+
const dimension = "- **Dimension 1.1** — DONE — fixture behaviour → Test `fixture_test`";
|
|
78
|
+
expect(validateSpec(VALID.replace(dimension, `${dimension}\n${dimension}`), COMMANDS, EXISTS)).toContain("Dimension identifiers must be unique");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("all declared lanes are required for a source change", () => {
|
|
82
|
+
const commands = { ...COMMANDS, "verify.integration": [["cargo", "test", "--test", "system"]], "verify.lint": [["check", "style"]] };
|
|
83
|
+
const text = VALID.replace("Fixture content for Files Changed (blast radius).", "`lib/widget.rs` | EDIT | Implement behavior.");
|
|
84
|
+
const errors = validateSpec(text, commands, EXISTS).join("\n");
|
|
85
|
+
expect(errors).toContain("cargo test --test system");
|
|
86
|
+
expect(errors).toContain("check style");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("the rubric lists conditional suites without guessing their applicability from prose", () => {
|
|
90
|
+
const commands = { ...COMMANDS, "verify.integration": [["unused"]], "build": [["unrelated"]] };
|
|
91
|
+
expect(validateSpec(VALID, commands, EXISTS)).toEqual(["rubric misses declared command verbatim: unused"]);
|
|
92
|
+
const text = VALID.replace(RUBRIC_ROW, `${RUBRIC_ROW}\n| S2 | Conditional check | \`unused\` | gate reports pass or no code | P0 | |`);
|
|
93
|
+
expect(validateSpec(text, commands, EXISTS)).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("documentation verification does not require an application test lane", () => {
|
|
97
|
+
expect(validateSpec(VALID, { conform: [["true"]], "verify.docs": [["true"]] }, EXISTS)).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("the Markdown parser handles escaped pipes inside command arguments", () => {
|
|
101
|
+
const commands = { conform: [["check", "a|b"]] };
|
|
102
|
+
const text = VALID.replace("`true`", "`check a\\|b`");
|
|
103
|
+
expect(validateSpec(text, commands, EXISTS)).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("oversize specs report their measured size against the cap", () => {
|
|
107
|
+
expect(validateSpec(`${VALID}\n${"line\n".repeat(320)}`, COMMANDS, EXISTS).join("\n")).toMatch(/spec has \d+ lines; cap 320/);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { auditSpec } from "../audits/spec-template";
|
|
5
|
+
import { UNSCOPED_ENVIRONMENT } from "./git_env";
|
|
6
|
+
import { cleanupTemporaryDirectories, git, modelFor, newSpecRepository, ROOT, SPEC_RELATIVE, specFixture } from "./gates_test_support";
|
|
7
|
+
|
|
8
|
+
const CONFIG_PATH = ".oracle/orly.json";
|
|
9
|
+
const STAGED = "--staged";
|
|
10
|
+
const FILE = "--file";
|
|
11
|
+
const RESIDUE = "path/to/file.ext";
|
|
12
|
+
const GOOD = specFixture();
|
|
13
|
+
const VALID_CONFIG = { commands: { conform: [["true"]], "verify.unit": [["true"]] } };
|
|
14
|
+
|
|
15
|
+
afterEach(cleanupTemporaryDirectories);
|
|
16
|
+
|
|
17
|
+
function run(project: string, ...args: string[]) {
|
|
18
|
+
const result = Bun.spawnSync(["bash", join(ROOT, "audits/spec-template.sh"), ...args], {
|
|
19
|
+
cwd: project, env: UNSCOPED_ENVIRONMENT, stdout: "pipe", stderr: "pipe",
|
|
20
|
+
});
|
|
21
|
+
return { code: result.exitCode, output: `${result.stdout}${result.stderr}` };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("spec audit through real Git and file boundaries", () => {
|
|
25
|
+
test("an invalid index fails even when its working copy was repaired", async () => {
|
|
26
|
+
const project = newSpecRepository();
|
|
27
|
+
await modelFor(project);
|
|
28
|
+
await Bun.write(join(project, SPEC_RELATIVE), `${GOOD}\n${RESIDUE}\n`);
|
|
29
|
+
git(project, "add", SPEC_RELATIVE);
|
|
30
|
+
await Bun.write(join(project, SPEC_RELATIVE), GOOD);
|
|
31
|
+
const result = run(project, STAGED);
|
|
32
|
+
expect(result.code).toBe(1);
|
|
33
|
+
expect(result.output).toContain("unfilled template placeholder");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a repaired index passes despite invalid unstaged content and config", async () => {
|
|
37
|
+
const project = newSpecRepository();
|
|
38
|
+
await modelFor(project);
|
|
39
|
+
await Bun.write(join(project, SPEC_RELATIVE), `${GOOD}\nReady.\n`);
|
|
40
|
+
git(project, "add", SPEC_RELATIVE);
|
|
41
|
+
await Bun.write(join(project, SPEC_RELATIVE), RESIDUE);
|
|
42
|
+
await Bun.write(join(project, CONFIG_PATH), "invalid json");
|
|
43
|
+
const result = run(project, STAGED);
|
|
44
|
+
expect(result.code).toBe(0);
|
|
45
|
+
expect(result.output).toContain("sections, references, test mappings");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("read-first references are resolved from the index", async () => {
|
|
49
|
+
const project = newSpecRepository();
|
|
50
|
+
await modelFor(project);
|
|
51
|
+
git(project, "rm", "README.md");
|
|
52
|
+
await Bun.write(join(project, "README.md"), "unstaged replacement");
|
|
53
|
+
const errors = await auditSpec([SPEC_RELATIVE, SPEC_RELATIVE, STAGED], project);
|
|
54
|
+
expect(errors.join("\n")).toContain("pointer does not resolve: README.md");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("file mode checks working content and configuration", async () => {
|
|
58
|
+
const project = newSpecRepository();
|
|
59
|
+
await modelFor(project);
|
|
60
|
+
expect(await auditSpec([SPEC_RELATIVE, SPEC_RELATIVE, FILE], project)).toEqual([]);
|
|
61
|
+
await Bun.write(join(project, CONFIG_PATH), JSON.stringify({ commands: { "verify.custom": [["custom", "check"]] } }));
|
|
62
|
+
expect((await auditSpec([SPEC_RELATIVE, SPEC_RELATIVE, FILE], project)).join("\n")).toContain("custom check");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test.each([
|
|
66
|
+
["invalid JSON", "{"],
|
|
67
|
+
["missing commands", "{}"],
|
|
68
|
+
["empty commands", '{"commands":{}}'],
|
|
69
|
+
["empty group", '{"commands":{"conform":[]}}'],
|
|
70
|
+
["invalid invocation", '{"commands":{"conform":[[]]}}'],
|
|
71
|
+
["nonstring argument", '{"commands":{"conform":[[3]]}}'],
|
|
72
|
+
["nonarray group", '{"commands":{"conform":true}}'],
|
|
73
|
+
])("refuses %s without skipping parity", async (_label, config) => {
|
|
74
|
+
const project = newSpecRepository();
|
|
75
|
+
await Bun.write(join(project, CONFIG_PATH), config);
|
|
76
|
+
await expect(auditSpec([SPEC_RELATIVE, SPEC_RELATIVE, FILE], project)).rejects.toThrow();
|
|
77
|
+
expect(run(project, FILE, SPEC_RELATIVE).code).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("missing config, staged config, and arguments fail explicitly", async () => {
|
|
81
|
+
const project = newSpecRepository();
|
|
82
|
+
await expect(auditSpec([], project)).rejects.toThrow("expected spec path");
|
|
83
|
+
await expect(auditSpec([SPEC_RELATIVE, SPEC_RELATIVE, FILE], project)).rejects.toThrow();
|
|
84
|
+
await Bun.write(join(project, CONFIG_PATH), JSON.stringify(VALID_CONFIG));
|
|
85
|
+
await expect(auditSpec([SPEC_RELATIVE, SPEC_RELATIVE, STAGED], project)).rejects.toThrow("from the index");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("bulk scans retain their prohibited-pattern scope", async () => {
|
|
89
|
+
const project = newSpecRepository();
|
|
90
|
+
await Bun.write(join(project, SPEC_RELATIVE), "# historical incomplete spec\n");
|
|
91
|
+
expect(run(project, "--all").code).toBe(0);
|
|
92
|
+
await Bun.write(join(project, SPEC_RELATIVE), "# Spec\n## Estimated effort\n");
|
|
93
|
+
const result = run(project, "--all");
|
|
94
|
+
expect(result.code).toBe(1);
|
|
95
|
+
expect(result.output).toContain("Estimated effort");
|
|
96
|
+
});
|
|
97
|
+
});
|
package/src/validation.ts
CHANGED
|
@@ -42,6 +42,15 @@ export function validateCommands(label: string, value: unknown, errors: string[]
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export function commandSetupErrors(commands: Record<string, string[][]>): string[] {
|
|
46
|
+
const errors: string[] = [];
|
|
47
|
+
if (!commands.conform?.length) errors.push("declare conform in .oracle/orly.json for work checks");
|
|
48
|
+
if (!Object.keys(commands).some((name) => /^verify\..+/.test(name) && commands[name]?.length)) {
|
|
49
|
+
errors.push("declare at least one verify.* command in .oracle/orly.json (for example, verify.unit or verify.docs)");
|
|
50
|
+
}
|
|
51
|
+
return errors;
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
export function validateActiveRule(
|
|
46
55
|
key: string,
|
|
47
56
|
rule: JsonObject,
|