@printpractical/cruze 0.0.1 → 0.0.2
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/README.md +38 -14
- package/dist/adapters/inbound/cli/cli_context.js +1 -0
- package/dist/adapters/inbound/cli/commands/code.js +2 -1
- package/dist/adapters/inbound/cli/commands/install.js +4 -0
- package/dist/adapters/inbound/cli/commands/realign.js +28 -0
- package/dist/adapters/inbound/cli/commands.js +2 -0
- package/dist/adapters/inbound/cli/parse_command.js +2 -0
- package/dist/adapters/inbound/cli/run_cli.js +9 -0
- package/dist/app/project_context.js +4 -2
- package/dist/app/use_cases/check_code.js +8 -3
- package/dist/app/use_cases/install_skills.js +8 -1
- package/dist/app/use_cases/journal_events.js +4 -3
- package/dist/app/use_cases/land_change.js +27 -6
- package/dist/app/use_cases/realign_done.js +45 -0
- package/dist/app/use_cases/realign_status.js +106 -0
- package/dist/app/use_cases/record_version.js +56 -0
- package/dist/app/use_cases/run_review.js +1 -1
- package/dist/app/use_cases/validate_project.js +10 -4
- package/dist/domain/check/check_source.js +23 -2
- package/dist/domain/config.js +10 -0
- package/dist/domain/journal.js +29 -5
- package/dist/domain/project/layout.js +1 -0
- package/dist/domain/status/next_step.js +19 -2
- package/dist/domain/validation/plan_rules.js +18 -1
- package/dist/domain/validation/project_rules.js +17 -1
- package/dist/domain/validation/validate_project.js +2 -1
- package/dist/domain/validation/work_item_rules.js +7 -0
- package/dist/domain/versions/ci_pin.js +40 -0
- package/dist/domain/versions/cruze_version.js +51 -0
- package/dist/domain/versions/realign_notes.js +72 -0
- package/dist/domain/versions/rule_versions.js +22 -0
- package/dist/domain/versions/scheduled_exceptions.js +41 -0
- package/package.json +1 -1
- package/skills/architect/feature.md +4 -2
- package/skills/behavioural-testing/SKILL.md +6 -0
- package/skills/build/SKILL.md +1 -1
- package/skills/envision/project.md +4 -2
- package/skills/formats/reference/config.md +6 -2
- package/skills/formats/reference/work-items.md +1 -1
- package/skills/hexagonal-design/SKILL.md +7 -1
- package/skills/hexagonal-design/languages/rust.md +7 -2
- package/skills/land/SKILL.md +1 -1
- package/skills/next/SKILL.md +1 -0
- package/skills/plan/tasks.md +7 -3
- package/skills/plan/test-plan.md +1 -1
- package/skills/realign/SKILL.md +35 -0
- package/skills/realign/notes/0.0.2.md +69 -0
- package/skills/realign/notes.md +29 -0
- package/skills/research/SKILL.md +2 -1
- package/skills/rethink/SKILL.md +3 -1
- package/skills/rethink/levels.md +1 -1
- package/skills/roles/SKILL.md +5 -2
- package/skills/roles/auditor.md +35 -0
- package/skills/roles/code-reviewer.md +4 -3
- package/skills/roles/design-reviewer.md +9 -6
- package/skills/roles/researcher.md +1 -0
- package/src/adapters/inbound/cli/cli_context.ts +1 -0
- package/src/adapters/inbound/cli/commands/code.ts +2 -1
- package/src/adapters/inbound/cli/commands/install.ts +4 -0
- package/src/adapters/inbound/cli/commands/realign.ts +28 -0
- package/src/adapters/inbound/cli/commands.ts +2 -0
- package/src/adapters/inbound/cli/parse_command.ts +2 -0
- package/src/adapters/inbound/cli/run_cli.ts +9 -0
- package/src/app/project_context.ts +4 -2
- package/src/app/use_cases/check_code.ts +8 -3
- package/src/app/use_cases/install_skills.ts +8 -1
- package/src/app/use_cases/journal_events.ts +4 -3
- package/src/app/use_cases/land_change.ts +27 -6
- package/src/app/use_cases/realign_done.ts +45 -0
- package/src/app/use_cases/realign_status.ts +106 -0
- package/src/app/use_cases/record_version.ts +56 -0
- package/src/app/use_cases/run_review.ts +1 -1
- package/src/app/use_cases/validate_project.ts +10 -4
- package/src/domain/check/check_source.ts +23 -2
- package/src/domain/config.ts +10 -0
- package/src/domain/journal.ts +29 -5
- package/src/domain/project/layout.ts +1 -0
- package/src/domain/status/next_step.ts +19 -2
- package/src/domain/validation/plan_rules.ts +18 -1
- package/src/domain/validation/project_rules.ts +17 -1
- package/src/domain/validation/validate_project.ts +2 -1
- package/src/domain/validation/work_item_rules.ts +7 -0
- package/src/domain/versions/ci_pin.ts +40 -0
- package/src/domain/versions/cruze_version.ts +51 -0
- package/src/domain/versions/realign_notes.ts +72 -0
- package/src/domain/versions/rule_versions.ts +22 -0
- package/src/domain/versions/scheduled_exceptions.ts +41 -0
- package/templates/init/config.yaml +4 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { realignDone } from "../../../../app/use_cases/realign_done.ts";
|
|
2
|
+
import { realignStatus } from "../../../../app/use_cases/realign_status.ts";
|
|
3
|
+
import { UsageError, requireArgs, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
|
|
4
|
+
|
|
5
|
+
export async function runRealign(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
|
|
6
|
+
requireArgs(args, ["status|done"]);
|
|
7
|
+
if (args[0] === "status") {
|
|
8
|
+
const report = await realignStatus(context, { full: options.full });
|
|
9
|
+
const lines = [`Code meets Cruze ${report.standards}; the installed skills are ${report.target}${report.full ? " (full audit)" : ""}.`];
|
|
10
|
+
for (const note of report.notes) {
|
|
11
|
+
const open = note.findings.filter((f) => !f.waived);
|
|
12
|
+
const how = note.detect !== undefined ? `${open.length} open finding(s) from ${note.detect}` : `for the auditor: ${note.ask ?? ""}`;
|
|
13
|
+
lines.push(`${note.version} ${note.id}: ${note.title} (${how})`);
|
|
14
|
+
for (const f of open) lines.push(` ${f.path}${f.line === undefined ? "" : `:${f.line}`} ${f.message}`);
|
|
15
|
+
}
|
|
16
|
+
if (report.newWorkOnly.length > 0) lines.push(`New work only, nothing to move: ${report.newWorkOnly.join(", ")}`);
|
|
17
|
+
if (report.notes.length === 0) lines.push("Nothing to realign.");
|
|
18
|
+
return { json: report, human: lines.join("\n") };
|
|
19
|
+
}
|
|
20
|
+
if (args[0] === "done") {
|
|
21
|
+
const report = await realignDone(context, options.change === undefined ? {} : { change: options.change });
|
|
22
|
+
const lines = [report.from === report.to ? `The code already meets Cruze ${report.to}.` : `The code now meets Cruze ${report.to}, up from ${report.from}.`];
|
|
23
|
+
if (report.scheduled !== undefined) lines.push(`${report.scheduled} fixes the rest.`);
|
|
24
|
+
if (report.excepted.length > 0) lines.push(`Listed under check.exceptions until it lands: ${report.excepted.join(", ")}`);
|
|
25
|
+
return { json: report, human: lines.join("\n") };
|
|
26
|
+
}
|
|
27
|
+
throw new UsageError(`cruze realign takes status or done, not "${args[0]}"`);
|
|
28
|
+
}
|
|
@@ -3,6 +3,7 @@ import { runCheck, runLand, runTrace } from "./commands/code.ts";
|
|
|
3
3
|
import { runApprove, runNew, runValidate } from "./commands/documents.ts";
|
|
4
4
|
import { runInit } from "./commands/init.ts";
|
|
5
5
|
import { runInstall } from "./commands/install.ts";
|
|
6
|
+
import { runRealign } from "./commands/realign.ts";
|
|
6
7
|
import { runFeedback, runJournal } from "./commands/journal.ts";
|
|
7
8
|
import { runAbandon, runFeatures, runRoadmap, runTask } from "./commands/progress.ts";
|
|
8
9
|
import { runReviewCommand } from "./commands/review.ts";
|
|
@@ -26,6 +27,7 @@ export const COMMANDS: Record<string, { handler: Handler; usage: string }> = {
|
|
|
26
27
|
check: { handler: runCheck, usage: "check [<file>...] [--ci] enforce layer rules and file budgets" },
|
|
27
28
|
abandon: { handler: runAbandon, usage: "abandon <ref> --reason <text> archive a feature or standalone change that stops for good" },
|
|
28
29
|
land: { handler: runLand, usage: "land [<ref>] [--override <reason>] merge a verified change into the living docs" },
|
|
30
|
+
realign: { handler: runRealign, usage: "realign <status [--full]|done [--change <ref>]> what the installed Cruze expects of existing code, and recording that the code meets it" },
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export const USAGE = `Usage: cruze <command> [options]
|
|
@@ -41,6 +41,7 @@ export function parseCommand(argv: string[]): ParsedCommand {
|
|
|
41
41
|
gate: { type: "string" },
|
|
42
42
|
override: { type: "string" },
|
|
43
43
|
overlap: { type: "boolean", default: false },
|
|
44
|
+
full: { type: "boolean", default: false },
|
|
44
45
|
round: { type: "string" },
|
|
45
46
|
package: { type: "string" },
|
|
46
47
|
replan: { type: "string", multiple: true, default: [] },
|
|
@@ -65,6 +66,7 @@ export function parseCommand(argv: string[]): ParsedCommand {
|
|
|
65
66
|
file: values.file,
|
|
66
67
|
ci: values.ci,
|
|
67
68
|
overlap: values.overlap,
|
|
69
|
+
full: values.full,
|
|
68
70
|
replan: values.replan,
|
|
69
71
|
rebase: values.rebase,
|
|
70
72
|
...optional("name", values.name),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { versionNotice } from "../../../app/use_cases/record_version.ts";
|
|
1
2
|
import { CruzeError } from "../../../domain/cruze_error.ts";
|
|
2
3
|
import { type CliContext, UsageError } from "./cli_context.ts";
|
|
3
4
|
import { COMMANDS, USAGE } from "./commands.ts";
|
|
@@ -10,12 +11,17 @@ export interface Terminal {
|
|
|
10
11
|
stdoutIsTerminal: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/** Commands that set the version themselves, or report the skew as a problem of their own. */
|
|
15
|
+
const NO_VERSION_NOTICE = new Set(["init", "install", "validate"]);
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* Runs one CLI invocation and returns the exit code: 0 on success, 1 when a check fails or
|
|
15
19
|
* an expected error occurs, 2 for usage errors. Agents get JSON on stdout; people get a summary on stderr.
|
|
20
|
+
* When the project's skills don't match this CLI, a note on stderr says so, even when the command fails.
|
|
16
21
|
*/
|
|
17
22
|
export async function runCli(argv: string[], context: CliContext, terminal: Terminal): Promise<number> {
|
|
18
23
|
let json = !terminal.stdoutIsTerminal || argv.includes("--json");
|
|
24
|
+
let notice: string | undefined;
|
|
19
25
|
try {
|
|
20
26
|
const parsed = parseCommand(argv);
|
|
21
27
|
json ||= parsed.options.json;
|
|
@@ -30,6 +36,7 @@ export async function runCli(argv: string[], context: CliContext, terminal: Term
|
|
|
30
36
|
}
|
|
31
37
|
const command = COMMANDS[name];
|
|
32
38
|
if (command === undefined) throw new UsageError(`Unknown command: ${name}`);
|
|
39
|
+
if (!NO_VERSION_NOTICE.has(name)) notice = await versionNotice(context.files, context.bundle.version);
|
|
33
40
|
const result = await command.handler(context, args, parsed.options);
|
|
34
41
|
terminal.stderr(result.human);
|
|
35
42
|
if (json) terminal.stdout(JSON.stringify(result.json, null, 2));
|
|
@@ -46,5 +53,7 @@ export async function runCli(argv: string[], context: CliContext, terminal: Term
|
|
|
46
53
|
return 1;
|
|
47
54
|
}
|
|
48
55
|
throw error;
|
|
56
|
+
} finally {
|
|
57
|
+
if (notice !== undefined) terminal.stderr(`cruze: note: ${notice}`);
|
|
49
58
|
}
|
|
50
59
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildProjectView, type ProjectView } from "../domain/project/project_view.ts";
|
|
2
|
-
import { SNAPSHOT_ROOTS } from "../domain/project/layout.ts";
|
|
2
|
+
import { PATHS, SNAPSHOT_ROOTS } from "../domain/project/layout.ts";
|
|
3
3
|
import { journalPath, serializeEntry, type JournalEntry } from "../domain/journal.ts";
|
|
4
4
|
import type { Bundle } from "./ports/bundle.ts";
|
|
5
5
|
import type { Clock } from "./ports/clock.ts";
|
|
@@ -14,7 +14,7 @@ export interface ProjectDeps {
|
|
|
14
14
|
bundle: Bundle;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/** Reads every document under docs/ and .cruze
|
|
17
|
+
/** Reads every document under docs/ and .cruze/, and the README, into a view. */
|
|
18
18
|
export async function loadView(files: ProjectFiles): Promise<ProjectView> {
|
|
19
19
|
const snapshot = new Map<string, string>();
|
|
20
20
|
for (const root of SNAPSHOT_ROOTS) {
|
|
@@ -23,6 +23,8 @@ export async function loadView(files: ProjectFiles): Promise<ProjectView> {
|
|
|
23
23
|
if (text !== undefined) snapshot.set(path, text);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
const readme = await files.readText(PATHS.readme);
|
|
27
|
+
if (readme !== undefined) snapshot.set(PATHS.readme, readme);
|
|
26
28
|
return buildProjectView(snapshot);
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -2,6 +2,7 @@ import { factsOf } from "../../domain/elements.ts";
|
|
|
2
2
|
import { checkSource, type CheckFinding } from "../../domain/check/check_source.ts";
|
|
3
3
|
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
4
4
|
import { matchesAny } from "../../domain/glob.ts";
|
|
5
|
+
import { FIRST_RELEASE, isPending } from "../../domain/versions/rule_versions.ts";
|
|
5
6
|
import type { CruzeConfig } from "../../domain/config.ts";
|
|
6
7
|
import type { ProjectView } from "../../domain/project/project_view.ts";
|
|
7
8
|
import { resolveChange } from "../../domain/project/artifact_ref.ts";
|
|
@@ -18,7 +19,8 @@ export interface CheckReport {
|
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Enforces layer rules and file budgets. Layer violations always fail; budget and
|
|
21
|
-
* module-map warnings fail only with `ci`.
|
|
22
|
+
* module-map warnings fail only with `ci`. A rule newer than the project's `standards:`
|
|
23
|
+
* is pending: it reports, but fails nothing until the project realigns.
|
|
22
24
|
*/
|
|
23
25
|
export async function checkCode(deps: ProjectDeps, options: { files?: string[]; ci: boolean }): Promise<CheckReport> {
|
|
24
26
|
const view = await loadView(deps.files);
|
|
@@ -32,8 +34,11 @@ export async function checkCode(deps: ProjectDeps, options: { files?: string[];
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
const only = options.files?.filter((file) => sources.has(file));
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
+
const standards = config.standards ?? FIRST_RELEASE;
|
|
38
|
+
const findings = checkSource({ config, sources, modulePaths: modulePaths(view), ...(only === undefined ? {} : { only }) }).map((f) =>
|
|
39
|
+
isPending("check", f.rule, standards) ? { ...f, pending: true as const } : f,
|
|
40
|
+
);
|
|
41
|
+
const failing = findings.filter((f) => f.pending !== true && (f.severity === "error" || options.ci));
|
|
37
42
|
return { passed: failing.length === 0, checked: only?.length ?? sources.size, findings };
|
|
38
43
|
}
|
|
39
44
|
|
|
@@ -4,6 +4,7 @@ import { AGENT_SKILL_HOMES, SHARED_SKILLS_DIR, agentSkillHome, knownAgents } fro
|
|
|
4
4
|
import { SKILL_FILE, installedSkillName, isInstalledSkillName, skillProblems } from "../../domain/skill.ts";
|
|
5
5
|
import type { Bundle, BundledSkill } from "../ports/bundle.ts";
|
|
6
6
|
import type { ProjectFiles } from "../ports/project_files.ts";
|
|
7
|
+
import { recordVersion, refuseDowngrade, type VersionRecord } from "./record_version.ts";
|
|
7
8
|
|
|
8
9
|
export interface InstallSkillsRequest {
|
|
9
10
|
/** Agents to link skills for even when the project shows no sign of using them. */
|
|
@@ -15,12 +16,16 @@ export interface InstallSkillsReport {
|
|
|
15
16
|
installed: string[];
|
|
16
17
|
removed: string[];
|
|
17
18
|
linked: Array<{ agent: string; dir: string }>;
|
|
19
|
+
/** The version recorded in .cruze/config.yaml, and the CI workflow's Cruze pin. */
|
|
20
|
+
recorded: VersionRecord;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Replaces the project's Cruze skills with the bundled set. Skills are written to
|
|
22
25
|
* the shared Agent Skills directory, and linked for every agent the project uses.
|
|
23
26
|
* Skills without the Cruze prefix belong to the user and are never touched.
|
|
27
|
+
* The project then records this version, and its CI runs it too. An older CLI
|
|
28
|
+
* never replaces the skills of a project that moved to a newer one.
|
|
24
29
|
*/
|
|
25
30
|
export async function installSkills(
|
|
26
31
|
deps: { files: ProjectFiles; bundle: Bundle },
|
|
@@ -29,6 +34,7 @@ export async function installSkills(
|
|
|
29
34
|
const { files, bundle } = deps;
|
|
30
35
|
const skills = await bundle.skills();
|
|
31
36
|
assertValid(skills);
|
|
37
|
+
await refuseDowngrade(files, bundle.version);
|
|
32
38
|
|
|
33
39
|
const names = skills.map((skill) => installedSkillName(skill.folder));
|
|
34
40
|
const removed = await removeStaleSkills(files, SHARED_SKILLS_DIR, names);
|
|
@@ -48,7 +54,8 @@ export async function installSkills(
|
|
|
48
54
|
linked.push({ agent, dir: home.skillsDir });
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
const recorded = await recordVersion(files, bundle.version);
|
|
58
|
+
return { version: bundle.version, installed: names, removed, linked, recorded };
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
function assertValid(skills: BundledSkill[]): void {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
2
|
-
import { recordedEventProblems, type JournalEntry } from "../../domain/journal.ts";
|
|
2
|
+
import { recordedEventProblems, recordedFields, type JournalEntry } from "../../domain/journal.ts";
|
|
3
3
|
import { resolveArtifact } from "../../domain/project/artifact_ref.ts";
|
|
4
4
|
import { appendJournal, loadView, type ProjectDeps } from "../project_context.ts";
|
|
5
5
|
|
|
@@ -8,13 +8,14 @@ export async function recordEvent(deps: ProjectDeps, event: string, fields: Reco
|
|
|
8
8
|
const problems = recordedEventProblems(event, fields);
|
|
9
9
|
if (problems.length > 0) throw new CruzeError("invalid-event", problems.join("; "));
|
|
10
10
|
let folder = ".cruze";
|
|
11
|
+
let recorded = recordedFields(fields);
|
|
11
12
|
if (item !== undefined) {
|
|
12
13
|
const resolved = resolveArtifact(await loadView(deps.files), item).item;
|
|
13
14
|
if (resolved === undefined) throw new CruzeError("not-found", `"${item}" is not a feature or change`);
|
|
14
15
|
folder = resolved.folder;
|
|
15
|
-
|
|
16
|
+
recorded = { ...recorded, item: resolved.ref };
|
|
16
17
|
}
|
|
17
|
-
return appendJournal(deps, folder, event, { ...
|
|
18
|
+
return appendJournal(deps, folder, event, { ...recorded, cruze: deps.bundle.version });
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export async function listEvents(deps: ProjectDeps, event?: string): Promise<JournalEntry[]> {
|
|
@@ -15,6 +15,7 @@ import { changesOf, featureOf, type WorkItem } from "../../domain/project/work_i
|
|
|
15
15
|
import { isVerified } from "../../domain/status/verification.ts";
|
|
16
16
|
import { activeChange } from "../../domain/status/work_status.ts";
|
|
17
17
|
import { validateProject } from "../../domain/validation/validate_project.ts";
|
|
18
|
+
import { withoutScheduledExceptions } from "../../domain/versions/scheduled_exceptions.ts";
|
|
18
19
|
import { appendJournal, loadView, withFiles, type ProjectDeps } from "../project_context.ts";
|
|
19
20
|
|
|
20
21
|
const AGENTS_FILE = "AGENTS.md";
|
|
@@ -26,6 +27,8 @@ export interface LandReport {
|
|
|
26
27
|
removed: string[];
|
|
27
28
|
restamped: string[];
|
|
28
29
|
finished: boolean;
|
|
30
|
+
/** Files whose `check.exceptions` entry `realign` scheduled in this change, removed now that it lands. */
|
|
31
|
+
unexcepted: string[];
|
|
29
32
|
archivedTo?: string;
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -54,7 +57,9 @@ export async function landChange(deps: ProjectDeps, ref?: string, options: { ove
|
|
|
54
57
|
const landed = `${deps.clock.today()}${commit === null ? "" : ` (${commit})`}`;
|
|
55
58
|
const books = applyBookkeeping(view, change, source, landed, plan.files);
|
|
56
59
|
const merged = withFiles(view, plan.files);
|
|
57
|
-
|
|
60
|
+
const recorded = { ...(merged.approvals.get(source.folder) ?? { version: 1, approvals: [], merged: {} }), merged: plan.merged };
|
|
61
|
+
const checked = withFiles(merged, new Map([[approvalsPath(source.folder), serializeApprovals(recorded)]]));
|
|
62
|
+
requireStillValid(view, checked, [source.path, ...changesOf(view.items, source).map((c) => c.path)]);
|
|
58
63
|
|
|
59
64
|
const approvals = new Map<string, ApprovalsFile>();
|
|
60
65
|
const by = (await deps.repository.userName()) ?? "unknown";
|
|
@@ -81,8 +86,9 @@ export async function landChange(deps: ProjectDeps, ref?: string, options: { ove
|
|
|
81
86
|
const rendered = renderAgentsFile(agents, merged);
|
|
82
87
|
if (rendered !== agents) await deps.files.writeText(AGENTS_FILE, rendered);
|
|
83
88
|
}
|
|
89
|
+
const unexcepted = await clearScheduledExceptions(deps, [change.ref, source.ref]);
|
|
84
90
|
|
|
85
|
-
const report: LandReport = { change: change.ref, merged: plan.mergedIds, built: plan.built, removed: plan.removed, restamped: restamp, finished: books.finished };
|
|
91
|
+
const report: LandReport = { change: change.ref, merged: plan.mergedIds, built: plan.built, removed: plan.removed, restamped: restamp, finished: books.finished, unexcepted };
|
|
86
92
|
if (books.archive !== undefined) report.archivedTo = books.archive.to;
|
|
87
93
|
return report;
|
|
88
94
|
}
|
|
@@ -102,12 +108,27 @@ function requireReady(view: ProjectView, change: WorkItem, source: WorkItem): vo
|
|
|
102
108
|
if (reasons.length > 0) throw new CruzeError("not-ready", `${change.ref} cannot land yet:\n ${reasons.join("\n ")}`);
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
/** The merge may not introduce errors into the living docs. */
|
|
106
|
-
function requireStillValid(before: ProjectView, after: ProjectView): void {
|
|
111
|
+
/** The merge may not introduce errors into the living docs, or into the work it lands, such as a citation of an ID it removes. */
|
|
112
|
+
function requireStillValid(before: ProjectView, after: ProjectView, work: string[]): void {
|
|
107
113
|
const key = (p: { path: string; message: string }): string => `${p.path}|${p.message}`;
|
|
108
114
|
const existing = new Set(validateProject(before).map(key));
|
|
109
|
-
const
|
|
115
|
+
const watched = (path: string): boolean => path.startsWith("docs/") || work.includes(path);
|
|
116
|
+
const introduced = validateProject(after).filter((p) => p.severity === "error" && watched(p.path) && !existing.has(key(p)));
|
|
110
117
|
if (introduced.length > 0) {
|
|
111
|
-
throw new CruzeError("merge-invalid", `landing would
|
|
118
|
+
throw new CruzeError("merge-invalid", `landing would leave errors behind; nothing was written:\n ${introduced.map((p) => `${p.path}:${p.line ?? ""} ${p.message}`).join("\n ")}`);
|
|
112
119
|
}
|
|
113
120
|
}
|
|
121
|
+
|
|
122
|
+
/** Removes the exceptions `realign` scheduled in the landed work, so CI checks those files again. */
|
|
123
|
+
async function clearScheduledExceptions(deps: ProjectDeps, refs: string[]): Promise<string[]> {
|
|
124
|
+
let text = await deps.files.readText(PATHS.config);
|
|
125
|
+
if (text === undefined) return [];
|
|
126
|
+
const removed: string[] = [];
|
|
127
|
+
for (const ref of [...new Set(refs)]) {
|
|
128
|
+
const result = withoutScheduledExceptions(text, ref);
|
|
129
|
+
text = result.text;
|
|
130
|
+
removed.push(...result.removed);
|
|
131
|
+
}
|
|
132
|
+
if (removed.length > 0) await deps.files.writeText(PATHS.config, text);
|
|
133
|
+
return removed;
|
|
134
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
2
|
+
import { resolveArtifact } from "../../domain/project/artifact_ref.ts";
|
|
3
|
+
import { PATHS } from "../../domain/project/layout.ts";
|
|
4
|
+
import { withVersionLine } from "../../domain/versions/cruze_version.ts";
|
|
5
|
+
import { withScheduledExceptions } from "../../domain/versions/scheduled_exceptions.ts";
|
|
6
|
+
import { appendJournal, loadView, type ProjectDeps } from "../project_context.ts";
|
|
7
|
+
import { realignStatus } from "./realign_status.ts";
|
|
8
|
+
|
|
9
|
+
export interface RealignDone {
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
/** The change that fixes what is still open, when anything is. */
|
|
13
|
+
scheduled?: string;
|
|
14
|
+
/** Files listed under `check.exceptions` until that change lands. */
|
|
15
|
+
excepted: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Moves `standards:` up to the installed Cruze once every mechanical finding is fixed, waived,
|
|
20
|
+
* or scheduled in a change. A scheduled `check` finding becomes an exception naming that change,
|
|
21
|
+
* so CI stays green until it lands, and landing it removes the exception.
|
|
22
|
+
*/
|
|
23
|
+
export async function realignDone(deps: ProjectDeps, options: { change?: string }): Promise<RealignDone> {
|
|
24
|
+
const status = await realignStatus(deps, { full: false });
|
|
25
|
+
if (status.standards === status.target) return { from: status.standards, to: status.target, excepted: [] };
|
|
26
|
+
const open = status.notes.flatMap((note) => note.findings.filter((f) => !f.waived).map((f) => ({ note, finding: f })));
|
|
27
|
+
let scheduled: string | undefined;
|
|
28
|
+
if (open.length > 0) {
|
|
29
|
+
if (options.change === undefined) {
|
|
30
|
+
const list = open.map(({ note, finding }) => `${note.id}: ${finding.path}${finding.line === undefined ? "" : `:${finding.line}`}`);
|
|
31
|
+
throw new CruzeError("realign-open", `fix, waive or schedule these first, then pass --change <ref> for the change that fixes the rest:\n ${list.join("\n ")}`);
|
|
32
|
+
}
|
|
33
|
+
const item = resolveArtifact(await loadView(deps.files), options.change).item;
|
|
34
|
+
if (item === undefined) throw new CruzeError("not-found", `"${options.change}" is not a feature or change; create the prefactor change first`);
|
|
35
|
+
scheduled = item.ref;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const excepted = open.filter(({ note }) => note.detect?.startsWith("check:") === true).map(({ note, finding }) => ({ path: finding.path, note: note.id }));
|
|
39
|
+
const config = (await deps.files.readText(PATHS.config)) ?? "";
|
|
40
|
+
const withExceptions = scheduled === undefined ? config : withScheduledExceptions(config, status.target, scheduled, excepted);
|
|
41
|
+
await deps.files.writeText(PATHS.config, withVersionLine(withExceptions, "standards", status.target));
|
|
42
|
+
const paths = [...new Set(excepted.map((e) => e.path))];
|
|
43
|
+
await appendJournal(deps, ".cruze", "realign", { from: status.standards, to: status.target, ...(scheduled === undefined ? {} : { scheduled }), excepted: paths });
|
|
44
|
+
return { from: status.standards, to: status.target, ...(scheduled === undefined ? {} : { scheduled }), excepted: paths };
|
|
45
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
2
|
+
import type { CruzeConfig } from "../../domain/config.ts";
|
|
3
|
+
import { PATHS } from "../../domain/project/layout.ts";
|
|
4
|
+
import type { ProjectView } from "../../domain/project/project_view.ts";
|
|
5
|
+
import { validateProject } from "../../domain/validation/validate_project.ts";
|
|
6
|
+
import { notesBetween, parseRealignNotes, type RealignNote } from "../../domain/versions/realign_notes.ts";
|
|
7
|
+
import { FIRST_RELEASE } from "../../domain/versions/rule_versions.ts";
|
|
8
|
+
import type { Bundle } from "../ports/bundle.ts";
|
|
9
|
+
import { loadView, type ProjectDeps } from "../project_context.ts";
|
|
10
|
+
import { checkCode } from "./check_code.ts";
|
|
11
|
+
|
|
12
|
+
export const REALIGN_SKILL = "realign";
|
|
13
|
+
|
|
14
|
+
export interface RealignFinding {
|
|
15
|
+
path: string;
|
|
16
|
+
line?: number;
|
|
17
|
+
message: string;
|
|
18
|
+
/** Waived by an earlier realign, so never raised again. */
|
|
19
|
+
waived: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RealignNoteReport extends RealignNote {
|
|
23
|
+
/** What the note's CLI rule reports now, for a note with `Detect`. */
|
|
24
|
+
findings: RealignFinding[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RealignStatus {
|
|
28
|
+
/** The version the code meets now, and the one the installed skills expect. */
|
|
29
|
+
standards: string;
|
|
30
|
+
target: string;
|
|
31
|
+
full: boolean;
|
|
32
|
+
/** Notes for existing code: those with findings to fix, waive or schedule, and those an auditor answers. */
|
|
33
|
+
notes: RealignNoteReport[];
|
|
34
|
+
/** Notes that change only new work, so nothing existing has to move. */
|
|
35
|
+
newWorkOnly: string[];
|
|
36
|
+
/** Mechanical findings not yet waived. */
|
|
37
|
+
open: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What realigning the project to its installed Cruze involves: the notes of every release
|
|
42
|
+
* since `standards:`, or of all releases with `full`, and what their CLI rules find now.
|
|
43
|
+
*/
|
|
44
|
+
export async function realignStatus(deps: ProjectDeps, options: { full: boolean }): Promise<RealignStatus> {
|
|
45
|
+
const view = await loadView(deps.files);
|
|
46
|
+
const config = requireInstalled(view, deps.bundle.version);
|
|
47
|
+
const standards = config.standards ?? FIRST_RELEASE;
|
|
48
|
+
const target = deps.bundle.version;
|
|
49
|
+
const notes = notesBetween(await bundledNotes(deps.bundle), options.full ? "0.0.0" : standards, target);
|
|
50
|
+
const waived = waivedFindings(view);
|
|
51
|
+
const check = (await checkCode(deps, { ci: false })).findings;
|
|
52
|
+
const problems = validateProject(view);
|
|
53
|
+
const report = notes.filter((n) => n.appliesTo === "existing").map((note): RealignNoteReport => {
|
|
54
|
+
const [command, rule] = (note.detect ?? ":").split(":");
|
|
55
|
+
const found = command === "check" ? check.filter((f) => f.rule === rule) : command === "validate" ? problems.filter((p) => p.rule === rule) : [];
|
|
56
|
+
const findings = found.map((f) => ({
|
|
57
|
+
path: f.path,
|
|
58
|
+
...(f.line === undefined ? {} : { line: f.line }),
|
|
59
|
+
message: f.message,
|
|
60
|
+
// A check finding is waived by listing the file under check.exceptions, which also keeps CI green.
|
|
61
|
+
waived: command === "validate" && waived.has(findingKey(note.id, f.path)),
|
|
62
|
+
}));
|
|
63
|
+
return { ...note, findings };
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
standards,
|
|
67
|
+
target,
|
|
68
|
+
full: options.full,
|
|
69
|
+
notes: report,
|
|
70
|
+
newWorkOnly: notes.filter((n) => n.appliesTo === "new").map((n) => n.id),
|
|
71
|
+
open: report.flatMap((n) => n.findings).filter((f) => !f.waived).length,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The key a realign disposition records for a mechanical finding, so a waiver can be matched later. */
|
|
76
|
+
export function findingKey(note: string, path: string): string {
|
|
77
|
+
return `${note}: ${path}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The project's config, once its skills come from this CLI; realign works from the installed notes. */
|
|
81
|
+
export function requireInstalled(view: ProjectView, version: string): CruzeConfig {
|
|
82
|
+
const config = view.config?.config;
|
|
83
|
+
if (config === null || config === undefined) throw new CruzeError("config", `fix ${PATHS.config} first; run cruze validate for details`);
|
|
84
|
+
if (config.cruze !== version) {
|
|
85
|
+
throw new CruzeError("install-first", `the project's skills are from Cruze ${config.cruze ?? "an unrecorded version"}, and this CLI is ${version}; run cruze install first`);
|
|
86
|
+
}
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function bundledNotes(bundle: Bundle): Promise<RealignNote[]> {
|
|
91
|
+
const skill = (await bundle.skills()).find((s) => s.folder === REALIGN_SKILL);
|
|
92
|
+
return (skill?.files ?? [])
|
|
93
|
+
.flatMap((file) => {
|
|
94
|
+
const version = /^notes\/(\d+\.\d+\.\d+)\.md$/.exec(file.path)?.[1];
|
|
95
|
+
return version === undefined ? [] : parseRealignNotes(version, file.text).notes;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Findings earlier realigns waived, by key. */
|
|
100
|
+
function waivedFindings(view: ProjectView): Set<string> {
|
|
101
|
+
return new Set(
|
|
102
|
+
view.journal
|
|
103
|
+
.filter((e) => e.event === "disposition" && e["review"] === "realign" && e["disposition"] === "waived")
|
|
104
|
+
.map((e) => String(e["finding"])),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
2
|
+
import { PATHS } from "../../domain/project/layout.ts";
|
|
3
|
+
import { repinCruze } from "../../domain/versions/ci_pin.ts";
|
|
4
|
+
import { compareVersions, versionSkew, withRecordedVersion } from "../../domain/versions/cruze_version.ts";
|
|
5
|
+
import type { ProjectFiles } from "../ports/project_files.ts";
|
|
6
|
+
|
|
7
|
+
export const CI_WORKFLOW = ".github/workflows/ci.yml";
|
|
8
|
+
|
|
9
|
+
export interface VersionRecord {
|
|
10
|
+
/** The version the project recorded before, when it recorded one. */
|
|
11
|
+
from?: string;
|
|
12
|
+
to: string;
|
|
13
|
+
/** The CI workflow's Cruze specs after the edit, and those left for the user to move. */
|
|
14
|
+
ci: { repinned: string[]; unrecognised: string[] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The version the project's config records, read without requiring the rest of the config to be valid. */
|
|
18
|
+
export async function recordedVersion(files: ProjectFiles): Promise<string | undefined> {
|
|
19
|
+
const text = await files.readText(PATHS.config);
|
|
20
|
+
return text === undefined ? undefined : versionIn(text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What to tell the user when a Cruze project's skills don't match this CLI; nothing outside a Cruze project. */
|
|
24
|
+
export async function versionNotice(files: ProjectFiles, version: string): Promise<string | undefined> {
|
|
25
|
+
const text = await files.readText(PATHS.config);
|
|
26
|
+
return text === undefined ? undefined : versionSkew(version, versionIn(text))?.message;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function versionIn(configText: string): string | undefined {
|
|
30
|
+
return /^cruze:\s*["']?([^"'\s#]+)/m.exec(configText)?.[1];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Refuses to install older skills over a project that already moved to a newer Cruze. */
|
|
34
|
+
export async function refuseDowngrade(files: ProjectFiles, version: string): Promise<void> {
|
|
35
|
+
const recorded = await recordedVersion(files);
|
|
36
|
+
if (recorded !== undefined && compareVersions(recorded, version) > 0) {
|
|
37
|
+
throw new CruzeError(
|
|
38
|
+
"older-cli",
|
|
39
|
+
`this project uses Cruze ${recorded}, and this CLI is ${version}; upgrade the CLI instead. To go back to ${version} on purpose, set cruze: in ${PATHS.config} first`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Records the installed version in the config, and moves the CI workflow's Cruze to it. */
|
|
45
|
+
export async function recordVersion(files: ProjectFiles, version: string): Promise<VersionRecord> {
|
|
46
|
+
const from = await recordedVersion(files);
|
|
47
|
+
const config = await files.readText(PATHS.config);
|
|
48
|
+
if (config !== undefined) {
|
|
49
|
+
const updated = withRecordedVersion(config, version);
|
|
50
|
+
if (updated !== config) await files.writeText(PATHS.config, updated);
|
|
51
|
+
}
|
|
52
|
+
const ci = await files.readText(CI_WORKFLOW);
|
|
53
|
+
const repin = ci === undefined ? { text: "", repinned: [], unrecognised: [] } : repinCruze(ci, version);
|
|
54
|
+
if (ci !== undefined && repin.text !== ci) await files.writeText(CI_WORKFLOW, repin.text);
|
|
55
|
+
return { ...(from === undefined ? {} : { from }), to: version, ci: { repinned: repin.repinned, unrecognised: repin.unrecognised } };
|
|
56
|
+
}
|
|
@@ -4,7 +4,7 @@ import { featureOf } from "../../domain/project/work_items.ts";
|
|
|
4
4
|
import type { AgentRunner } from "../ports/agent_runner.ts";
|
|
5
5
|
import { loadView, type ProjectDeps } from "../project_context.ts";
|
|
6
6
|
|
|
7
|
-
export const ROLES = ["design-reviewer", "code-reviewer", "verifier", "researcher"] as const;
|
|
7
|
+
export const ROLES = ["design-reviewer", "code-reviewer", "verifier", "researcher", "auditor"] as const;
|
|
8
8
|
export type Role = (typeof ROLES)[number];
|
|
9
9
|
|
|
10
10
|
export interface ReviewRequest {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { hasErrors, validateProject } from "../../domain/validation/validate_project.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { warning, type Problem } from "../../domain/validation/problem.ts";
|
|
3
|
+
import { PATHS } from "../../domain/project/layout.ts";
|
|
4
|
+
import { versionSkew } from "../../domain/versions/cruze_version.ts";
|
|
5
|
+
import type { Bundle } from "../ports/bundle.ts";
|
|
3
6
|
import { loadView } from "../project_context.ts";
|
|
4
7
|
import type { ProjectFiles } from "../ports/project_files.ts";
|
|
5
8
|
|
|
@@ -10,9 +13,12 @@ export interface ValidateReport {
|
|
|
10
13
|
problems: Problem[];
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
/** Checks every document in the project against the cruze-formats rules. */
|
|
14
|
-
export async function validate(deps: { files: ProjectFiles }): Promise<ValidateReport> {
|
|
15
|
-
const
|
|
16
|
+
/** Checks every document in the project against the cruze-formats rules, and that its skills match this CLI. */
|
|
17
|
+
export async function validate(deps: { files: ProjectFiles; bundle: Bundle }): Promise<ValidateReport> {
|
|
18
|
+
const view = await loadView(deps.files);
|
|
19
|
+
const config = view.config?.config;
|
|
20
|
+
const skew = config === null || config === undefined ? undefined : versionSkew(deps.bundle.version, config.cruze);
|
|
21
|
+
const problems = [...validateProject(view), ...(skew === undefined ? [] : [warning(PATHS.config, undefined, "cruze-version", skew.message)])];
|
|
16
22
|
return {
|
|
17
23
|
valid: !hasErrors(problems),
|
|
18
24
|
errors: problems.filter((p) => p.severity === "error").length,
|
|
@@ -6,10 +6,12 @@ import { countTypes } from "./type_count.ts";
|
|
|
6
6
|
export interface CheckFinding {
|
|
7
7
|
path: string;
|
|
8
8
|
line?: number;
|
|
9
|
-
rule: "layer" | "max-lines" | "max-types" | "outside-module-map" | "no-layer";
|
|
9
|
+
rule: "layer" | "max-lines" | "max-types" | "outside-module-map" | "no-layer" | "test-placement";
|
|
10
10
|
message: string;
|
|
11
11
|
/** Layer violations always fail; the rest fail in CI unless excepted. */
|
|
12
12
|
severity: "error" | "warning";
|
|
13
|
+
/** Set when the rule is newer than the project's `standards:`: it reports, but can't fail CI until the project realigns. */
|
|
14
|
+
pending?: true;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface CheckInput {
|
|
@@ -56,9 +58,28 @@ export function checkSource(input: CheckInput): CheckFinding[] {
|
|
|
56
58
|
if (!excepted && types !== null && types > config.check.maxTypes) {
|
|
57
59
|
findings.push({ path, rule: "max-types", severity: "warning", message: `${types} top-level types, over the budget of ${config.check.maxTypes}` });
|
|
58
60
|
}
|
|
59
|
-
if (
|
|
61
|
+
if (!excepted && isRustTestFileInSource(path)) {
|
|
62
|
+
findings.push({ path, rule: "test-placement", severity: "warning", message: "Rust tests that leave their inline module go in the crate's tests/ directory, not under src/" });
|
|
63
|
+
}
|
|
64
|
+
if (modulePaths.length > 0 && !insideModuleMap(path, modulePaths)) {
|
|
60
65
|
findings.push({ path, rule: "outside-module-map", severity: "warning", message: "the file is outside every module in the architecture's module map" });
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
return findings;
|
|
64
69
|
}
|
|
70
|
+
|
|
71
|
+
/** Whether a module's Path covers the file: a directory path ending in `/` covers everything under it, any other names one file. */
|
|
72
|
+
export function insideModuleMap(path: string, modulePaths: string[]): boolean {
|
|
73
|
+
return modulePaths.some((module) => (module.endsWith("/") ? path.startsWith(module) : path === module));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A Rust file of tests split out of a module: `tests.rs`, `*_tests.rs`, or anything under a `tests/` folder inside `src/`. */
|
|
77
|
+
function isRustTestFileInSource(path: string): boolean {
|
|
78
|
+
if (!path.endsWith(".rs")) return false;
|
|
79
|
+
const segments = path.split("/");
|
|
80
|
+
const inSource = segments.lastIndexOf("src");
|
|
81
|
+
if (inSource === -1) return false;
|
|
82
|
+
const below = segments.slice(inSource + 1);
|
|
83
|
+
const file = below.at(-1) ?? "";
|
|
84
|
+
return file === "tests.rs" || file.endsWith("_tests.rs") || below.slice(0, -1).includes("tests");
|
|
85
|
+
}
|
package/src/domain/config.ts
CHANGED
|
@@ -3,6 +3,10 @@ import { parse as parseYaml } from "yaml";
|
|
|
3
3
|
/** `.cruze/config.yaml`, as described in the cruze-formats skill. */
|
|
4
4
|
export interface CruzeConfig {
|
|
5
5
|
version: 1;
|
|
6
|
+
/** The Cruze version whose skills the project installed, recorded by `cruze install`. */
|
|
7
|
+
cruze?: string;
|
|
8
|
+
/** The Cruze version whose expectations the code meets, moved forward by `realign`. */
|
|
9
|
+
standards?: string;
|
|
6
10
|
project: string;
|
|
7
11
|
tracker: "markdown";
|
|
8
12
|
source: string[];
|
|
@@ -43,6 +47,10 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
43
47
|
if (root === null) return { config: null, problems: ["the file must be a YAML mapping"] };
|
|
44
48
|
|
|
45
49
|
if (root["version"] !== 1) problems.push("version must be 1");
|
|
50
|
+
const cruze = root["cruze"];
|
|
51
|
+
if (cruze !== undefined && (typeof cruze !== "string" || cruze.trim() === "")) problems.push("cruze must be a version string, such as \"0.0.2\"");
|
|
52
|
+
const standards = root["standards"];
|
|
53
|
+
if (standards !== undefined && (typeof standards !== "string" || standards.trim() === "")) problems.push("standards must be a version string, such as \"0.0.2\"");
|
|
46
54
|
if (typeof root["project"] !== "string" || root["project"] === "") problems.push("project must be a non-empty string");
|
|
47
55
|
if (root["tracker"] !== "markdown") problems.push("tracker must be markdown");
|
|
48
56
|
const source = stringList(root["source"], "source", problems);
|
|
@@ -92,6 +100,8 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
92
100
|
|
|
93
101
|
const config: CruzeConfig = {
|
|
94
102
|
version: 1,
|
|
103
|
+
...(typeof cruze === "string" && cruze.trim() !== "" ? { cruze } : {}),
|
|
104
|
+
...(typeof standards === "string" && standards.trim() !== "" ? { standards } : {}),
|
|
95
105
|
project: typeof root["project"] === "string" ? root["project"] : "",
|
|
96
106
|
tracker: "markdown",
|
|
97
107
|
source,
|