@drkmttr/adhere 0.21.2 → 0.23.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/README.md +28 -1552
- package/package.json +10 -7
- package/presets/README.md +9 -1
- package/presets/alchemy/runtime/bind-tag-not-implementation/RULE.md +1 -0
- package/presets/effect/data/brand-meaningful-primitives/RULE.md +1 -0
- package/presets/effect/data/variants-are-tagged-unions/RULE.md +1 -0
- package/presets/typescript/types/derive-dont-restate/RULE.md +1 -0
- package/skills/adhere/SKILL.md +194 -30
- package/skills/adhere-fix/SKILL.md +1 -1
- package/skills/adhere-setup/SKILL.md +49 -16
- package/src/cli.ts +216 -25
- package/src/config.ts +27 -4
- package/src/create/ask.ts +225 -9
- package/src/create/examples.ts +94 -0
- package/src/create/look.ts +70 -19
- package/src/create/oxlint-trial.ts +464 -0
- package/src/create/oxlint.ts +1012 -0
- package/src/create/trial-review.ts +562 -0
- package/src/create/trial.ts +611 -0
- package/src/create/wizard.ts +128 -94
- package/src/init.ts +169 -40
- package/src/install.ts +295 -93
- package/src/instructions.ts +34 -12
- package/src/markdown.ts +12 -2
- package/src/oxlint/catalog.json +7844 -0
- package/src/oxlint/catalog.ts +72 -0
- package/src/oxlint/shared.ts +140 -0
- package/src/readme-guide.ts +431 -0
- package/src/readme.ts +291 -0
- package/src/services/AdhereConfig.ts +36 -3
- package/src/services/AuditCache.ts +176 -169
- package/src/services/SourceWalker.ts +12 -5
- package/src/wording.ts +3 -3
- package/src/workflows/audit.ts +5 -2
- package/src/workflows/format.ts +1 -1
package/src/cli.ts
CHANGED
|
@@ -34,11 +34,32 @@ import { findContradictions, formatContradictions } from "#contradictions.ts";
|
|
|
34
34
|
import { createRule } from "#create/wizard.ts";
|
|
35
35
|
import { AGENTS } from "#create/agents.ts";
|
|
36
36
|
import { type Install, initProject } from "#init.ts";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
type CatalogMatch,
|
|
39
|
+
type InstallPicks,
|
|
40
|
+
type Source,
|
|
41
|
+
catalogOf,
|
|
42
|
+
cloned,
|
|
43
|
+
describeRules,
|
|
44
|
+
installSelected,
|
|
45
|
+
instructionPath,
|
|
46
|
+
listRules,
|
|
47
|
+
matchOf,
|
|
48
|
+
parseSource,
|
|
49
|
+
rulePath,
|
|
50
|
+
} from "#install.ts";
|
|
38
51
|
import { renderInstructions } from "#instructions.ts";
|
|
39
52
|
import { formatLinterCheck, tallyProjectRules } from "#mechanical.ts";
|
|
40
53
|
import { type PresetName, presetNames } from "#presets.ts";
|
|
41
54
|
import type { SavedKey } from "#providers/provider.ts";
|
|
55
|
+
import {
|
|
56
|
+
ReadmeUnavailable,
|
|
57
|
+
standardsRepoOf,
|
|
58
|
+
isSharedRepo,
|
|
59
|
+
originUrlAt,
|
|
60
|
+
refreshRemoteReadme,
|
|
61
|
+
refreshSharedReadme,
|
|
62
|
+
} from "#readme.ts";
|
|
42
63
|
import { globalRuleSet } from "#rules.ts";
|
|
43
64
|
import { formatStrays, straysAmong } from "#wording.ts";
|
|
44
65
|
import {
|
|
@@ -129,7 +150,7 @@ const sufficiencyThreshold = Flag.Finite("sufficiency-threshold").pipe(
|
|
|
129
150
|
const typeAware = Flag.Boolean("type-aware").pipe(
|
|
130
151
|
Flag.optional,
|
|
131
152
|
Flag.withDescription(
|
|
132
|
-
"Include compiler-resolved symbols and declaration excerpts. Off by default.",
|
|
153
|
+
"Include compiler-resolved symbols and declaration excerpts, and the bundled rules that declare them. Off by default.",
|
|
133
154
|
),
|
|
134
155
|
);
|
|
135
156
|
|
|
@@ -389,7 +410,7 @@ export const validateLayer = (flags: Flags) =>
|
|
|
389
410
|
|
|
390
411
|
/**
|
|
391
412
|
* `adhere validate`: everything `lint` loads, then how each rule's wording
|
|
392
|
-
* differs from the
|
|
413
|
+
* differs from the docs' rule writing tips, then the linter check's tallies
|
|
393
414
|
* from lint, then Jev on whether any two rules that apply to the same files
|
|
394
415
|
* contradict. Only a contradiction fails the run: the tips and the linter
|
|
395
416
|
* check are advice. A config, rule file, or preset that does not decode
|
|
@@ -419,7 +440,7 @@ export const validateCommand = Command.make("validate", { preset }, () =>
|
|
|
419
440
|
}),
|
|
420
441
|
).pipe(
|
|
421
442
|
Command.withDescription(
|
|
422
|
-
"Load the config, rule files, and presets the way lint does, check each rule's wording against the
|
|
443
|
+
"Load the config, rule files, and presets the way lint does, check each rule's wording against the docs' rule writing tips, report the rules lint's linter check flags, then ask Jev whether any two rules that apply to the same files contradict each other.",
|
|
423
444
|
),
|
|
424
445
|
Command.provide((input) => validateLayer({ presets: input.preset })),
|
|
425
446
|
);
|
|
@@ -442,7 +463,7 @@ const shared = Flag.String("shared").pipe(
|
|
|
442
463
|
),
|
|
443
464
|
Flag.optional,
|
|
444
465
|
Flag.withDescription(
|
|
445
|
-
"Scaffold a repo of rules
|
|
466
|
+
"Scaffold a repo of instructions, rules, and a lint package other repos use, named org/repo on GitHub: example instructions and rules, a README, a CI workflow that runs adhere validate, and an alchemy.run.ts that creates the repo, public unless you make it private when asked, and its TYPESAFE_API_KEY secret.",
|
|
446
467
|
),
|
|
447
468
|
);
|
|
448
469
|
|
|
@@ -471,7 +492,7 @@ export const initCommand = Command.make("init", { force, shared }, (input) =>
|
|
|
471
492
|
}),
|
|
472
493
|
).pipe(
|
|
473
494
|
Command.withDescription(
|
|
474
|
-
"Scaffold .adhere/config.ts and example Markdown rules, and add @drkmttr/adhere to devDependencies with the project's package manager.",
|
|
495
|
+
"Scaffold .adhere/config.ts, .adhere/tsconfig.json, and example Markdown rules, and add @drkmttr/adhere to devDependencies with the project's package manager.",
|
|
475
496
|
),
|
|
476
497
|
);
|
|
477
498
|
|
|
@@ -481,7 +502,7 @@ const source = Argument.String("source").pipe(
|
|
|
481
502
|
(spec) => `org/repo, then optionally a topic or rule and #ref, not ${spec}`,
|
|
482
503
|
),
|
|
483
504
|
Argument.withDescription(
|
|
484
|
-
"A GitHub repo with rules in .adhere/rules/, as org/repo, optionally followed by a topic or a rule, as in org/repo/data/brand-ports, and by #branch or #tag.",
|
|
505
|
+
"A GitHub repo with instructions in .adhere/instructions/ and rules in .adhere/rules/, as org/repo, optionally followed by a topic or a rule, as in org/repo/data/brand-ports, and by #branch or #tag.",
|
|
485
506
|
),
|
|
486
507
|
);
|
|
487
508
|
|
|
@@ -500,30 +521,136 @@ export const listCommand = Command.make("list", { source }, (input) =>
|
|
|
500
521
|
),
|
|
501
522
|
);
|
|
502
523
|
|
|
503
|
-
|
|
524
|
+
class InstallUnavailable extends Schema.TaggedError<InstallUnavailable>()("InstallUnavailable", {
|
|
525
|
+
message: Schema.String,
|
|
526
|
+
}) {}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* What `adhere install` copies: one exact rule without asking, everything
|
|
530
|
+
* when `--all` is given, or what the user picks at a terminal. Without a
|
|
531
|
+
* terminal, a choice refuses and says what `--all` would copy and which exact
|
|
532
|
+
* rule can be named instead. A choice already installed is preselected, so
|
|
533
|
+
* running install again offers to bring it up to date, and deselecting it
|
|
534
|
+
* leaves it as it is.
|
|
535
|
+
*/
|
|
536
|
+
const choosePicks = Effect.fn("install.choosePicks")(function* (
|
|
537
|
+
source: Source,
|
|
538
|
+
match: CatalogMatch,
|
|
539
|
+
options: { readonly all: boolean; readonly root: string; readonly clone: string },
|
|
540
|
+
) {
|
|
541
|
+
if (match.kind === "direct") {
|
|
542
|
+
return { instructions: [], rules: [match.rule.id] } satisfies InstallPicks;
|
|
543
|
+
}
|
|
544
|
+
if (options.all) {
|
|
545
|
+
return {
|
|
546
|
+
instructions: match.instructions,
|
|
547
|
+
rules: match.rules.map(({ id }) => id),
|
|
548
|
+
} satisfies InstallPicks;
|
|
549
|
+
}
|
|
550
|
+
const pinned = source.ref === undefined ? "" : `#${source.ref}`;
|
|
551
|
+
const stdio = yield* Stdio.Stdio;
|
|
552
|
+
if (!(yield* stdio.stdinIsTerminal) || !(yield* stdio.stdoutIsTerminal)) {
|
|
553
|
+
const [first] = match.rules;
|
|
554
|
+
const ids = [
|
|
555
|
+
...match.instructions.map((file) => `instructions/${file}`),
|
|
556
|
+
...match.rules.map(({ id }) => id),
|
|
557
|
+
];
|
|
558
|
+
const spec = `${source.repo}${source.rule === "" ? "" : `/${source.rule}`}${pinned}`;
|
|
559
|
+
const exact =
|
|
560
|
+
first === undefined
|
|
561
|
+
? ""
|
|
562
|
+
: `, or name one rule exactly, as in ${source.repo}/${first.id}${pinned}`;
|
|
563
|
+
return yield* new InstallUnavailable({
|
|
564
|
+
message: `adhere install ${spec}: no terminal to choose from. Pass --all to copy every match (${ids.join(", ")})${exact}.`,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
const fs = yield* FileSystem.FileSystem;
|
|
568
|
+
const path = yield* Path.Path;
|
|
569
|
+
type Pick =
|
|
570
|
+
| { readonly kind: "instruction"; readonly path: string }
|
|
571
|
+
| { readonly kind: "rule"; readonly id: string };
|
|
572
|
+
const instructionChoices = yield* Effect.forEach(match.instructions, (file) =>
|
|
573
|
+
fs.exists(path.join(options.root, instructionPath(source, file))).pipe(
|
|
574
|
+
Effect.orElseSucceed(() => false),
|
|
575
|
+
Effect.map((selected) => ({
|
|
576
|
+
title: `instructions/${file}`,
|
|
577
|
+
value: { kind: "instruction", path: file } as const,
|
|
578
|
+
selected,
|
|
579
|
+
})),
|
|
580
|
+
),
|
|
581
|
+
);
|
|
582
|
+
const described = yield* describeRules(source, options.clone, match.rules);
|
|
583
|
+
const ruleChoices = yield* Effect.forEach(described, (rule) =>
|
|
584
|
+
fs.exists(path.join(options.root, rulePath(source, rule.id))).pipe(
|
|
585
|
+
Effect.orElseSucceed(() => false),
|
|
586
|
+
Effect.map((selected) => ({
|
|
587
|
+
title: rule.id,
|
|
588
|
+
description: rule.description,
|
|
589
|
+
value: { kind: "rule", id: rule.id } as const,
|
|
590
|
+
selected,
|
|
591
|
+
})),
|
|
592
|
+
),
|
|
593
|
+
);
|
|
594
|
+
const picks: ReadonlyArray<Pick> = yield* Prompt.run(
|
|
595
|
+
Prompt.MultiSelect<Pick>({
|
|
596
|
+
message:
|
|
597
|
+
source.rule === ""
|
|
598
|
+
? `What should adhere copy from ${source.repo}${pinned}?`
|
|
599
|
+
: `Which rules under ${source.repo}/${source.rule}${pinned} should adhere copy?`,
|
|
600
|
+
choices: [...instructionChoices, ...ruleChoices],
|
|
601
|
+
}),
|
|
602
|
+
);
|
|
603
|
+
return {
|
|
604
|
+
instructions: picks.flatMap((pick) => (pick.kind === "instruction" ? [pick.path] : [])),
|
|
605
|
+
rules: picks.flatMap((pick) => (pick.kind === "rule" ? [pick.id] : [])),
|
|
606
|
+
} satisfies InstallPicks;
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
/** `adhere install org/repo`: another repo's instructions and rules, copied into this one's `.adhere/`. */
|
|
504
610
|
export const installCommand = Command.make(
|
|
505
611
|
"install",
|
|
506
612
|
{
|
|
507
613
|
source,
|
|
508
614
|
force: Flag.Boolean("force").pipe(
|
|
509
615
|
Flag.withDefault(false),
|
|
510
|
-
Flag.withDescription(
|
|
616
|
+
Flag.withDescription(
|
|
617
|
+
"Replace instruction files and rules already in .adhere/, file by file and directory by directory.",
|
|
618
|
+
),
|
|
619
|
+
),
|
|
620
|
+
all: Flag.Boolean("all").pipe(
|
|
621
|
+
Flag.withDefault(false),
|
|
622
|
+
Flag.withDescription(
|
|
623
|
+
"Copy every instruction file and rule the source names, without asking. Needs no terminal.",
|
|
624
|
+
),
|
|
511
625
|
),
|
|
512
626
|
},
|
|
513
627
|
(input) =>
|
|
514
|
-
Effect.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
628
|
+
Effect.scoped(
|
|
629
|
+
Effect.gen(function* () {
|
|
630
|
+
const path = yield* Path.Path;
|
|
631
|
+
const root = path.resolve();
|
|
632
|
+
const clone = yield* cloned(input.source);
|
|
633
|
+
const catalog = yield* catalogOf(input.source, clone);
|
|
634
|
+
const match = yield* matchOf(input.source, catalog);
|
|
635
|
+
const picks = yield* choosePicks(input.source, match, {
|
|
636
|
+
all: input.all,
|
|
637
|
+
root,
|
|
638
|
+
clone,
|
|
639
|
+
});
|
|
640
|
+
const result = yield* installSelected(root, input.source, clone, picks, {
|
|
641
|
+
force: input.force,
|
|
642
|
+
});
|
|
643
|
+
yield* Console.log(
|
|
644
|
+
[
|
|
645
|
+
result.created.length > 0 ? `created: ${result.created.join(", ")}` : "created: none",
|
|
646
|
+
result.skipped.length > 0 ? `skipped: ${result.skipped.join(", ")}` : "skipped: none",
|
|
647
|
+
].join("\n"),
|
|
648
|
+
);
|
|
649
|
+
}),
|
|
650
|
+
),
|
|
524
651
|
).pipe(
|
|
525
652
|
Command.withDescription(
|
|
526
|
-
"Copy
|
|
653
|
+
"Copy instruction files and rules from a GitHub repo's .adhere/ into this repo's .adhere/instructions/<org>/<repo>/ and .adhere/rules/<org>/<repo>/. At a terminal, choose which instruction files and rules to copy; name a topic for its rules, or name one rule exactly to copy it without asking. --all copies everything. Existing files are skipped unless --force.",
|
|
527
654
|
),
|
|
528
655
|
);
|
|
529
656
|
|
|
@@ -645,13 +772,70 @@ const rulesCreateCommand = Command.make(
|
|
|
645
772
|
(input) => createRule(input.idea, input.agent, input.setup).pipe(Effect.provide(statusLayer)),
|
|
646
773
|
).pipe(
|
|
647
774
|
Command.withDescription(
|
|
648
|
-
"Write a rule with a coding agent's help: it suggests whether the rule belongs to this project or the organization's shared rules repo, names it, chooses a RULE.md or, when Jev needs state the file cannot show, a RULE.ts, and says which config context (typeAware, reads) it needs. It finds code in the repo for you to judge, with the lines around it, and drafts the rule from your answers; the draft is checked as adhere validate checks wording. A project rule is written to .adhere/rules/; an organization rule is pushed to the shared repo on a branch for a pull request.",
|
|
775
|
+
"Write a rule with a coding agent's help: it suggests whether the convention is a native oxlint rule (an id from adhere's catalog; unknown ids are rejected, with its exact option values and the config to print or enable in the shared repo), a mechanical rule (an oxlint JS plugin, approved before it runs and tested on Node 22 or newer; a failed test gets one repair and reapproval), or one adhere must judge. It then suggests whether the rule belongs to this project or the organization's shared rules repo, names it, chooses a RULE.md or, when Jev needs state the file cannot show, a RULE.ts, and says which config context (typeAware, reads) it needs. It finds code in the repo for you to judge, with the lines around it, and drafts the rule from your answers; the draft is checked as adhere validate checks wording. A project rule is written to .adhere/rules/ or .adhere/oxlint/; an organization rule is pushed to the shared repo on a branch for a pull request.",
|
|
776
|
+
),
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
/** The shared rules repo to refresh instead of this checkout's, as `org/repo`. */
|
|
780
|
+
const sharedRepo = Argument.String("repo").pipe(
|
|
781
|
+
Argument.filterMap(
|
|
782
|
+
(spec) => Option.fromNullishOr(/^[\w.-]+\/[\w.-]+$/.test(spec) ? spec : undefined),
|
|
783
|
+
(spec) => `org/repo, as on GitHub, not ${spec}`,
|
|
784
|
+
),
|
|
785
|
+
Argument.optional,
|
|
786
|
+
Argument.withDescription(
|
|
787
|
+
"The shared rules repo to refresh, as org/repo, instead of this checkout or the org's standards repo this checkout's origin names.",
|
|
788
|
+
),
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* `adhere rules readme [org/repo]`: refresh the managed index in a shared
|
|
793
|
+
* rules repo's README from the rules and lint config checked out there. In a
|
|
794
|
+
* shared repo it writes that README; otherwise it clones the GitHub repo named
|
|
795
|
+
* here, or the org's standards repo this checkout's origin names, and pushes the refreshed
|
|
796
|
+
* README on a review branch for a pull request.
|
|
797
|
+
*/
|
|
798
|
+
const rulesReadmeCommand = Command.make("readme", { repo: sharedRepo }, ({ repo }) =>
|
|
799
|
+
Effect.gen(function* () {
|
|
800
|
+
const path = yield* Path.Path;
|
|
801
|
+
const root = path.resolve();
|
|
802
|
+
const named = Option.isSome(repo) ? repo.value : undefined;
|
|
803
|
+
if (named === undefined && (yield* isSharedRepo(root))) {
|
|
804
|
+
const refreshed = yield* refreshSharedReadme(root);
|
|
805
|
+
yield* Console.log(refreshed.message);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const origin = named === undefined ? yield* originUrlAt(root) : undefined;
|
|
809
|
+
// A consumer repo's shared rules live in its org's standards repo, never in the repo itself.
|
|
810
|
+
const target = named ?? (origin === undefined ? undefined : standardsRepoOf(origin));
|
|
811
|
+
if (target === undefined) {
|
|
812
|
+
return yield* ReadmeUnavailable.make({
|
|
813
|
+
message:
|
|
814
|
+
origin === undefined
|
|
815
|
+
? `${root} is not a shared rules repo, and git names no origin here, so there is no README to refresh. Run this in the shared repo, name it as adhere rules readme org/repo, or add a GitHub origin.`
|
|
816
|
+
: `${root} is not a shared rules repo, and its origin ${origin} is not a GitHub repo. Run this in the shared repo, or name one: adhere rules readme org/repo.`,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
const refreshed = yield* refreshRemoteReadme(target);
|
|
820
|
+
yield* Console.log(
|
|
821
|
+
refreshed.branch === undefined
|
|
822
|
+
? `${refreshed.message}\nNo change, so nothing was pushed.`
|
|
823
|
+
: [
|
|
824
|
+
refreshed.message,
|
|
825
|
+
`Pushed ${refreshed.branch} to ${refreshed.repo}.`,
|
|
826
|
+
`Open the pull request: https://github.com/${refreshed.repo}/compare/${refreshed.branch}?expand=1`,
|
|
827
|
+
].join("\n"),
|
|
828
|
+
);
|
|
829
|
+
}).pipe(Effect.provide(statusLayer)),
|
|
830
|
+
).pipe(
|
|
831
|
+
Command.withDescription(
|
|
832
|
+
"Refresh the managed index in a shared rules repo's README from .adhere/rules/ and oxlint/rules.json: each rule's link and description, each enabled native Oxlint setting, and each custom plugin's enabled rules. In the shared repo it writes that README. Elsewhere it refreshes the GitHub repo named as org/repo, or the org's standards repo this checkout's origin names, from a clone, pushes README.md alone on a review branch, and prints the pull request link; a current README pushes nothing. It preserves the rest of the README and refuses malformed markers.",
|
|
649
833
|
),
|
|
650
834
|
);
|
|
651
835
|
|
|
652
836
|
export const rulesCommand = Command.make("rules").pipe(
|
|
653
837
|
Command.withDescription("Work on this repo's rules."),
|
|
654
|
-
Command.withSubcommands([rulesCreateCommand]),
|
|
838
|
+
Command.withSubcommands([rulesCreateCommand, rulesReadmeCommand]),
|
|
655
839
|
);
|
|
656
840
|
|
|
657
841
|
export const postinstallCommand = Command.make("postinstall", {}, () =>
|
|
@@ -659,13 +843,20 @@ export const postinstallCommand = Command.make("postinstall", {}, () =>
|
|
|
659
843
|
const path = yield* Path.Path;
|
|
660
844
|
yield* Console.log(yield* renderInstructions(path.resolve()));
|
|
661
845
|
}),
|
|
662
|
-
).pipe(
|
|
846
|
+
).pipe(
|
|
847
|
+
Command.withDescription(
|
|
848
|
+
"Render instruction files recursively into a managed block in AGENTS.md.",
|
|
849
|
+
),
|
|
850
|
+
);
|
|
663
851
|
|
|
664
852
|
/** The whole CLI. Bare `adhere` prints its help. */
|
|
665
853
|
export const cli = Command.make("adhere").pipe(
|
|
666
854
|
Command.withDescription("Lint a repository against the rules in .adhere/rules/, judged by Jev."),
|
|
667
855
|
Command.withExamples([
|
|
668
|
-
{
|
|
856
|
+
{
|
|
857
|
+
command: "adhere init",
|
|
858
|
+
description: "Scaffold config, tsconfig, and two example rules",
|
|
859
|
+
},
|
|
669
860
|
{
|
|
670
861
|
command: "adhere validate",
|
|
671
862
|
description: "Check the rules' wording, and ask Jev whether any contradict",
|
|
@@ -674,7 +865,7 @@ export const cli = Command.make("adhere").pipe(
|
|
|
674
865
|
{ command: "adhere lint", description: "Audit the working directory" },
|
|
675
866
|
{
|
|
676
867
|
command: "adhere install org/repo",
|
|
677
|
-
description: "Copy another repo's
|
|
868
|
+
description: "Copy another repo's instructions and rules into this one",
|
|
678
869
|
},
|
|
679
870
|
{
|
|
680
871
|
command: "adhere lint --preset effect",
|
package/src/config.ts
CHANGED
|
@@ -57,8 +57,9 @@ export type AppendState = (
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* What the config can have Jev read beside each file's code, shared by all
|
|
60
|
-
* its rules.
|
|
61
|
-
*
|
|
60
|
+
* its rules. A rule may also declare the reads it needs, which tells the run
|
|
61
|
+
* what would help without enabling any of it. Each name is the key the state
|
|
62
|
+
* carries it under, which a rule's description can name in backticks. `workspacePackages` maps only workspace
|
|
62
63
|
* packages imported by this file to their directories. `dependents` lists what
|
|
63
64
|
* depends on this file: workspace packages whose package.json names its package,
|
|
64
65
|
* and files that import it by a relative path. `symbols` maps names
|
|
@@ -116,6 +117,10 @@ export const Rule = Schema.Struct({
|
|
|
116
117
|
description:
|
|
117
118
|
"Where the rule does not apply: Jev is asked of each whether all the code that breaks the rule is as it describes, and a yes to any drops the finding.",
|
|
118
119
|
}),
|
|
120
|
+
reads: Schema.optionalKey(Reads).annotate({
|
|
121
|
+
description:
|
|
122
|
+
"Context the rule needs, such as `dependents` or `symbols`. This declares what would help; the config's `reads` and `typeAware` still decide what this run enables. A project's rule that declares a read they do not enable warns that it may be less accurate until enabled; a bundled preset rule that declares `symbols` or `references` is left out unless `typeAware` is on.",
|
|
123
|
+
}),
|
|
119
124
|
appendState: Schema.optionalKey(
|
|
120
125
|
Schema.declare((value: unknown): value is AppendState => typeof value === "function", {
|
|
121
126
|
expected: "a function",
|
|
@@ -225,7 +230,7 @@ export const AdhereConfig = Schema.Struct({
|
|
|
225
230
|
}),
|
|
226
231
|
typeAware: Schema.optionalKey(Schema.Boolean).annotate({
|
|
227
232
|
description:
|
|
228
|
-
"Include both compiler-resolved symbols and declaration excerpts. Defaults to false.",
|
|
233
|
+
"Include both compiler-resolved symbols and declaration excerpts. Defaults to false. Bundled preset rules that declare either run only with it on; a project's own rule still runs and warns.",
|
|
229
234
|
}),
|
|
230
235
|
maxSymbolLength: Schema.optionalKey(Schema.Natural).annotate({
|
|
231
236
|
description: "Maximum characters of each symbol's complete resolved content. Defaults to 256.",
|
|
@@ -262,6 +267,8 @@ export const AdhereConfig = Schema.Struct({
|
|
|
262
267
|
export const ADHERE_DIRECTORY = ".adhere";
|
|
263
268
|
/** Where a repo keeps its rules by default, one to a directory: `<id>/RULE.md` or `<id>/RULE.ts`. */
|
|
264
269
|
export const RULES_DIRECTORY = `${ADHERE_DIRECTORY}/rules`;
|
|
270
|
+
/** Where a repo keeps the Markdown instruction files postinstall renders into AGENTS.md. */
|
|
271
|
+
export const INSTRUCTIONS_DIRECTORY = `${ADHERE_DIRECTORY}/instructions`;
|
|
265
272
|
export const CACHE_DIRECTORY = `${ADHERE_DIRECTORY}/cache`;
|
|
266
273
|
/** Where a config may live, relative to the working directory. Exactly one may exist. */
|
|
267
274
|
export const CONFIG_FILES = [
|
|
@@ -406,6 +413,22 @@ export const presetsOf = (
|
|
|
406
413
|
});
|
|
407
414
|
};
|
|
408
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Whether the run enables the compiler's native reads: the command line
|
|
418
|
+
* replaces the config's opt-in, which defaults to off.
|
|
419
|
+
*/
|
|
420
|
+
export const typeAwareOf = (
|
|
421
|
+
config: Pick<typeof AdhereConfig.Type, "typeAware">,
|
|
422
|
+
flags: Flags,
|
|
423
|
+
): boolean => flags.typeAware ?? config.typeAware ?? false;
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Whether a rule declares a read only `typeAware` enables, so a run without
|
|
427
|
+
* it cannot judge the rule as it was written.
|
|
428
|
+
*/
|
|
429
|
+
export const declaresNativeReads = (rule: Pick<Rule, "reads">): boolean =>
|
|
430
|
+
(rule.reads ?? []).some((read) => read === "symbols" || read === "references");
|
|
431
|
+
|
|
409
432
|
/**
|
|
410
433
|
* Precedence, highest first: command line, config file, presets in order
|
|
411
434
|
* (a later preset wins), then the defaults. A config rule replaces a preset
|
|
@@ -422,7 +445,7 @@ export const resolveConfig = (
|
|
|
422
445
|
});
|
|
423
446
|
const last = <K extends "model" | "threshold">(key: K) =>
|
|
424
447
|
applied.map((preset) => preset[key]).findLast((value) => value !== undefined);
|
|
425
|
-
const typeAware =
|
|
448
|
+
const typeAware = typeAwareOf(config, flags);
|
|
426
449
|
return {
|
|
427
450
|
model: config.provider?.model ?? config.model ?? last("model") ?? DEFAULT_MODEL,
|
|
428
451
|
...(config.provider === undefined ? {} : { provider: config.provider }),
|