@drkmttr/adhere 0.17.5 → 0.18.1
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 +125 -64
- package/package.json +6 -6
- package/skills/adhere/SKILL.md +50 -33
- package/skills/adhere-setup/SKILL.md +71 -48
- package/src/cli.ts +13 -1
- package/src/config.ts +27 -3
- package/src/init.ts +22 -3
- package/src/install.ts +29 -23
- package/src/services/Jev.http.ts +20 -2
- package/src/services/Jev.provider.test.ts +280 -4
- package/src/services/Jev.reads.test.ts +99 -20
- package/src/services/Jev.ts +76 -12
- package/src/services/SourceReads.test.ts +290 -41
- package/src/services/SourceReads.ts +52 -29
- package/src/workflows/audit.ts +82 -11
package/src/workflows/audit.ts
CHANGED
|
@@ -6,25 +6,32 @@ import {
|
|
|
6
6
|
type ReadState,
|
|
7
7
|
type Rule,
|
|
8
8
|
type RuleId,
|
|
9
|
+
type Rules,
|
|
9
10
|
} from "#config.ts";
|
|
10
11
|
import { type Excerpt, type Range } from "#excerpt.ts";
|
|
11
12
|
import { type ScannedFile, WalkUnavailable } from "#models/Audit.ts";
|
|
12
13
|
import { AdhereConfig } from "#services/AdhereConfig.ts";
|
|
13
14
|
import { AuditCache, answersOf, type Judgment, sha256, type Tally } from "#services/AuditCache.ts";
|
|
14
15
|
import {
|
|
16
|
+
describeState,
|
|
15
17
|
fits,
|
|
16
18
|
Jev,
|
|
19
|
+
type JevBlocked,
|
|
20
|
+
type JevOverflow,
|
|
17
21
|
JevUnavailable,
|
|
18
22
|
type Judged,
|
|
19
23
|
judgeQuestion,
|
|
24
|
+
judgeBody,
|
|
20
25
|
judgeLoad,
|
|
21
26
|
judgeRequests,
|
|
22
27
|
linterQuestion,
|
|
23
28
|
type Located,
|
|
29
|
+
locateBody,
|
|
24
30
|
locateRequests,
|
|
25
31
|
matcherQuestions,
|
|
26
32
|
questionRoom,
|
|
27
33
|
tokensOf,
|
|
34
|
+
logRequestPayload,
|
|
28
35
|
} from "#services/Jev.ts";
|
|
29
36
|
import { type SourceReadRequest, SourceReads } from "#services/SourceReads.ts";
|
|
30
37
|
import { type SourceFile, SourceWalker } from "#services/SourceWalker.ts";
|
|
@@ -187,6 +194,7 @@ export interface FilePlan {
|
|
|
187
194
|
|
|
188
195
|
/** A run worked out from the files, the rules, and the cache, before any request. */
|
|
189
196
|
export interface AuditPlan {
|
|
197
|
+
readonly model: string;
|
|
190
198
|
readonly files: ReadonlyArray<FilePlan>;
|
|
191
199
|
/** The normalization used for content keys, also used when reloading a selected file. */
|
|
192
200
|
readonly includeComments?: boolean;
|
|
@@ -379,9 +387,11 @@ export const planAudit = (
|
|
|
379
387
|
? (config.workspacePackages ?? {})
|
|
380
388
|
: undefined;
|
|
381
389
|
const nativeReads: SourceReadRequest = {
|
|
382
|
-
symbols: config.
|
|
383
|
-
references: config.
|
|
390
|
+
symbols: config.typeAware,
|
|
391
|
+
references: config.typeAware,
|
|
384
392
|
includeComments: config.includeComments === true,
|
|
393
|
+
maxSymbolLength: config.maxSymbolLength,
|
|
394
|
+
maxSymbolDocumentationLength: config.maxSymbolDocumentationLength,
|
|
385
395
|
};
|
|
386
396
|
const walker = yield* SourceWalker;
|
|
387
397
|
const files = yield* walker.files;
|
|
@@ -504,6 +514,13 @@ export const planAudit = (
|
|
|
504
514
|
} else if (left <= 0) {
|
|
505
515
|
limited.push({ ...plan, pending: {}, deferred: true });
|
|
506
516
|
} else if (!fits(lines, rulesOf(checking), plan.readState)) {
|
|
517
|
+
yield* logRequestPayload(
|
|
518
|
+
`${plan.file.path}: too long preflight payload (planning)`,
|
|
519
|
+
() => ({
|
|
520
|
+
judge: judgeBody(config.model, lines, rulesOf(checking), [], plan.readState),
|
|
521
|
+
locate: locateBody(config.model, lines, rulesOf(checking), plan.readState),
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
507
524
|
limited.push({ ...plan, skipped: true, pending: {} });
|
|
508
525
|
} else {
|
|
509
526
|
left -= 1;
|
|
@@ -618,6 +635,7 @@ export const planAudit = (
|
|
|
618
635
|
judged.reduce((sum, plan) => sum + count(plan), 0);
|
|
619
636
|
const cachedLocateRequests = loads.reduce((sum, load) => sum + load.locating, 0);
|
|
620
637
|
return {
|
|
638
|
+
model: config.model,
|
|
621
639
|
files: planned,
|
|
622
640
|
...(config.includeComments === true ? { includeComments: true } : {}),
|
|
623
641
|
...(options.limit === 0 ? { planOnly: true } : {}),
|
|
@@ -725,8 +743,58 @@ export const executeAudit = (
|
|
|
725
743
|
),
|
|
726
744
|
};
|
|
727
745
|
}
|
|
728
|
-
|
|
729
|
-
|
|
746
|
+
}
|
|
747
|
+
let referencesOmitted = false;
|
|
748
|
+
const hasHooks = Object.values(checking).some(({ rule }) => rule.appendState !== undefined);
|
|
749
|
+
let referencesForHooks: unknown;
|
|
750
|
+
const omitReferences = (reason: string) =>
|
|
751
|
+
Effect.gen(function* () {
|
|
752
|
+
referencesForHooks = hasHooks ? readState.references : undefined;
|
|
753
|
+
readState = Record.filter(readState, (_, name) => name !== "references");
|
|
754
|
+
referencesOmitted = true;
|
|
755
|
+
yield* Effect.logDebug(`${file.path}: ${reason}; attempting without references`);
|
|
756
|
+
});
|
|
757
|
+
const rulesFor = (entries: FilePlan["prepared"]): Rules => {
|
|
758
|
+
const rules = rulesOf(entries);
|
|
759
|
+
if (!referencesOmitted) return rules;
|
|
760
|
+
return Record.map(rules, (rule) => {
|
|
761
|
+
const appendState = rule.appendState;
|
|
762
|
+
return appendState === undefined
|
|
763
|
+
? rule
|
|
764
|
+
: {
|
|
765
|
+
...rule,
|
|
766
|
+
appendState: async (state, file, bun) => {
|
|
767
|
+
const input =
|
|
768
|
+
referencesForHooks === undefined
|
|
769
|
+
? state
|
|
770
|
+
: describeState({ ...state, references: referencesForHooks });
|
|
771
|
+
return Record.filter(
|
|
772
|
+
await appendState(input, file, bun),
|
|
773
|
+
(_, name) => name !== "references",
|
|
774
|
+
);
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
});
|
|
778
|
+
};
|
|
779
|
+
const retryWithoutReferences = <A>(
|
|
780
|
+
work: () => Effect.Effect<A, JevUnavailable | JevOverflow | JevBlocked>,
|
|
781
|
+
) =>
|
|
782
|
+
Effect.suspend(work).pipe(
|
|
783
|
+
Effect.catchTag("JevOverflow", (problem) =>
|
|
784
|
+
referencesOmitted || (!("references" in readState) && !hasHooks)
|
|
785
|
+
? Effect.fail(problem)
|
|
786
|
+
: omitReferences("provider context limit").pipe(Effect.andThen(Effect.suspend(work))),
|
|
787
|
+
),
|
|
788
|
+
);
|
|
789
|
+
if (!isEmpty(checking) && !fits(file.lines, rulesOf(checking), readState)) {
|
|
790
|
+
yield* logRequestPayload(`${file.path}: too long preflight payload (full context)`, () => ({
|
|
791
|
+
judge: judgeBody(plan.model, file.lines, rulesOf(checking), [], readState),
|
|
792
|
+
locate: locateBody(plan.model, file.lines, rulesOf(checking), readState),
|
|
793
|
+
}));
|
|
794
|
+
if ("references" in readState) {
|
|
795
|
+
// Let the provider decide whether the reduced payload fits; do not veto the fallback.
|
|
796
|
+
yield* omitReferences("full context too long for Jev (preflight limit)");
|
|
797
|
+
} else {
|
|
730
798
|
yield* Effect.logDebug(`${file.path}: full context too long for Jev, skipped`);
|
|
731
799
|
return fileResult("skipped", findingsOf(filePlan, kept, original));
|
|
732
800
|
}
|
|
@@ -737,13 +805,13 @@ export const executeAudit = (
|
|
|
737
805
|
return k !== undefined && tokensOf(linterQuestion(k.rule)) <= room;
|
|
738
806
|
});
|
|
739
807
|
const sampleIds = Object.keys(sentSamples);
|
|
740
|
-
if (!isEmpty(pending))
|
|
741
|
-
sent(judgeRequests(file.lines, rulesOf(pending), sampleIds, readState));
|
|
742
808
|
const { probabilities, linter, matchers }: Judged = isEmpty(pending)
|
|
743
809
|
? { probabilities: {}, linter: {} }
|
|
744
|
-
: yield*
|
|
745
|
-
|
|
746
|
-
|
|
810
|
+
: yield* retryWithoutReferences(() => {
|
|
811
|
+
const rules = rulesFor(pending);
|
|
812
|
+
sent(judgeRequests(file.lines, rules, sampleIds, readState));
|
|
813
|
+
return jev.judge(file.lines, rules, sampleIds, asWritten, readState).pipe(inFile(file));
|
|
814
|
+
});
|
|
747
815
|
if (!isEmpty(linter)) yield* record(file.path, sentSamples, linter);
|
|
748
816
|
const judged: Record<RuleId, Judgment> = { ...kept };
|
|
749
817
|
for (const [id, probability] of Object.entries(probabilities)) {
|
|
@@ -763,7 +831,6 @@ export const executeAudit = (
|
|
|
763
831
|
}
|
|
764
832
|
|
|
765
833
|
const flagged = unlocated(prepared, judged);
|
|
766
|
-
if (!isEmpty(flagged)) sent(locateRequests(file.lines, rulesOf(flagged), readState));
|
|
767
834
|
// A blocked locate question still leaves the judgments worth caching: a rerun
|
|
768
835
|
// asks only for the sections again, not for every rule.
|
|
769
836
|
const {
|
|
@@ -774,7 +841,11 @@ export const executeAudit = (
|
|
|
774
841
|
readonly blocked: Blocked | undefined;
|
|
775
842
|
} = isEmpty(flagged)
|
|
776
843
|
? { places: {}, blocked: undefined }
|
|
777
|
-
: yield*
|
|
844
|
+
: yield* retryWithoutReferences(() => {
|
|
845
|
+
const rules = rulesFor(flagged);
|
|
846
|
+
sent(locateRequests(file.lines, rules, readState));
|
|
847
|
+
return jev.locate(file.lines, rules, asWritten, readState);
|
|
848
|
+
}).pipe(
|
|
778
849
|
Effect.map((places) => ({ places, blocked: undefined })),
|
|
779
850
|
Effect.catchTag("JevBlocked", ({ ray }) =>
|
|
780
851
|
Effect.succeed({ places: {}, blocked: { file: file.path, ray } }),
|