ast-mcp-server 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -1
- package/README.md +10 -3
- package/cordis.patch.yml +1 -1
- package/dist/angular-template-typecheck-entry.js +19 -0
- package/dist/services/angular-package-resolution.js +62 -0
- package/dist/services/angular-template-typecheck-core.js +139 -0
- package/dist/services/angular-template-typecheck-protocol.js +75 -0
- package/dist/services/angular-template-typecheck.js +262 -0
- package/dist/services/angular-templates.js +110 -59
- package/dist/services/context-builder.js +21 -2
- package/dist/services/explore-presentation.js +1 -0
- package/dist/services/h03-timeout-fixture.js +2 -1
- package/dist/services/operation-block-policy.js +19 -0
- package/dist/services/operation-plan-file.js +38 -4
- package/dist/services/operations.js +88 -15
- package/dist/services/project-operation-scheduler.js +4 -0
- package/dist/services/references.js +53 -28
- package/dist/tools/explore.js +3 -0
- package/dist/tools/find_references.js +6 -22
- package/dist/tools/operation-schema.js +43 -0
- package/dist/tools/rename_symbol.js +9 -4
- package/dist/tools/template-evidence-schema.js +25 -0
- package/package.json +10 -4
- package/skills/structural-code-editing/SKILL.md +8 -3
- package/skills/structural-code-editing/references/operations.md +5 -1
- package/skills/structural-code-editing/releases.json +31 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { readFile, stat } from "node:fs/promises";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import path from "node:path";
|
|
@@ -8,45 +8,17 @@ import { Node, SyntaxKind } from "ts-morph";
|
|
|
8
8
|
import { angularCoreMajor, componentDecorator, isCoreDecorator, isDirectiveClass, isMetadataPublic, isPipeClass, isSelectorPublic, literalString, bindingNames, metadataBindingNames, } from "./angular-decorators.js";
|
|
9
9
|
import { extractConsumerUsages, mayMatch, } from "./angular-template-bindings.js";
|
|
10
10
|
import { createScopeResolver, MAX_SCOPE_VISITS, ScopeWorkExhausted, } from "./angular-template-scope.js";
|
|
11
|
+
import { isSupportedVersion, isUnderAncestorNodeModules, isWithin, resolveFrom, } from "./angular-package-resolution.js";
|
|
11
12
|
import { assertSafeOperationFile } from "./safe-file.js";
|
|
12
13
|
const MAX_TEMPLATE_BYTES = 512 * 1024;
|
|
13
14
|
const MAX_TEMPLATE_NODES = 100_000;
|
|
14
15
|
const CHECKPOINT_EVERY_NODES = 1000;
|
|
15
|
-
const MIN_COMPILER_MAJOR = 18;
|
|
16
|
-
const MAX_COMPILER_MAJOR = 22;
|
|
17
16
|
const TWO_WAY_BINDING = 5; // BindingType.TwoWay, stable across supported majors
|
|
18
17
|
const TWO_WAY_EVENT = 2; // ParsedEventType.TwoWay, stable across supported majors
|
|
19
18
|
const SCAN_LIMITS = { templates: 2000, bytes: 32 * 1024 * 1024, nodes: 1_000_000 };
|
|
20
19
|
const NOT_APPLICABLE = { coverage: { status: "not_applicable" }, usages: [] };
|
|
21
20
|
const ASSIGNMENT_OPERATION = /^(?:=|(?:\*\*|&&|\|\||\?\?|<<|>>>|>>|[+\-*/%&|^])=)$/u;
|
|
22
21
|
const compilerCache = new Map();
|
|
23
|
-
function resolveFrom(tsConfigFilePath, request) {
|
|
24
|
-
try {
|
|
25
|
-
const resolved = createRequire(tsConfigFilePath).resolve(request);
|
|
26
|
-
return existsSync(resolved) ? resolved : undefined;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function isWithin(parent, child) {
|
|
33
|
-
const relative = path.relative(parent, child);
|
|
34
|
-
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
35
|
-
}
|
|
36
|
-
/** True when `packageDir` sits in a node_modules directory owned by `projectDir` or an ancestor. */
|
|
37
|
-
function isUnderAncestorNodeModules(packageDir, projectDir) {
|
|
38
|
-
const segments = packageDir.split(path.sep);
|
|
39
|
-
return segments.some((segment, index) => segment === "node_modules" &&
|
|
40
|
-
isWithin(segments.slice(0, index).join(path.sep) || path.sep, projectDir));
|
|
41
|
-
}
|
|
42
|
-
function isSupportedVersion(version) {
|
|
43
|
-
const match = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+.*)?$/u.exec(String(version));
|
|
44
|
-
if (!match)
|
|
45
|
-
return false;
|
|
46
|
-
const [major, minor, patch] = [match[1], match[2], match[3]].map(Number);
|
|
47
|
-
const belowFloor = major === MIN_COMPILER_MAJOR && minor === 0 && patch === 0 && !!match[4];
|
|
48
|
-
return major >= MIN_COMPILER_MAJOR && major <= MAX_COMPILER_MAJOR && !belowFloor;
|
|
49
|
-
}
|
|
50
22
|
async function loadCompiler(tsConfigFilePath) {
|
|
51
23
|
const unsupported = { reason: "compiler_unsupported" };
|
|
52
24
|
const manifestPath = resolveFrom(tsConfigFilePath, "@angular/compiler/package.json");
|
|
@@ -203,10 +175,10 @@ export function memberOwner(node) {
|
|
|
203
175
|
? { cls: owner, name: nameNode.getText() }
|
|
204
176
|
: undefined;
|
|
205
177
|
}
|
|
206
|
-
function hasDerivedComponent(
|
|
178
|
+
function hasDerivedComponent(cls, cache) {
|
|
207
179
|
const identity = (node) => `${node.getSourceFile().getFilePath()}:${node.getPos()}`;
|
|
208
180
|
const target = identity(cls);
|
|
209
|
-
return
|
|
181
|
+
return cache.components().some((candidate) => {
|
|
210
182
|
let base = candidate.getBaseClass();
|
|
211
183
|
for (let depth = 0; base && depth < 32; depth += 1) {
|
|
212
184
|
if (identity(base) === target)
|
|
@@ -323,9 +295,12 @@ function extractUsages(compiler, nodes, query, rc, onNode = () => { }) {
|
|
|
323
295
|
return collector.names;
|
|
324
296
|
};
|
|
325
297
|
class Templates extends compiler.TmplAstRecursiveVisitor {
|
|
298
|
+
// v18/v19 `tmplAstVisitAll` runs `visitor.visit(node) || node.visit(visitor)`; returning
|
|
299
|
+
// true marks the node handled so each subtree is walked once instead of twice per level.
|
|
326
300
|
visit(node) {
|
|
327
301
|
tick();
|
|
328
302
|
node.visit(this);
|
|
303
|
+
return true;
|
|
329
304
|
}
|
|
330
305
|
view(locals, children, before) {
|
|
331
306
|
const names = [...locals, ...referencesOf(children)].filter((name) => !!name);
|
|
@@ -431,6 +406,73 @@ function decodeTemplate(bytes) {
|
|
|
431
406
|
return undefined;
|
|
432
407
|
}
|
|
433
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Template work that does not depend on the queried symbol, shared by the collections of one
|
|
411
|
+
* request: the component list, each located template, each read and each parse. It is bound to the
|
|
412
|
+
* {@link ProjectContext} it was created for, and a collection over another context refuses it. It
|
|
413
|
+
* never outlives the request, so nothing is invalidated.
|
|
414
|
+
*
|
|
415
|
+
* Only whole results are kept. An entry is stored after its step returned, so a step that threw is
|
|
416
|
+
* run again by the next collection, and a scan that stopped early leaves complete entries for the
|
|
417
|
+
* templates it reached and none for the rest. Scan budgets are not part of it: every collection
|
|
418
|
+
* charges its own budget for each template, cached or not.
|
|
419
|
+
*/
|
|
420
|
+
export class TemplateScanCache {
|
|
421
|
+
context;
|
|
422
|
+
#components;
|
|
423
|
+
#located = new Map();
|
|
424
|
+
/** External templates by path, so a file shared by two components is read once; inline by location. */
|
|
425
|
+
#reads = new Map();
|
|
426
|
+
#parsed = new WeakMap();
|
|
427
|
+
#lines = new Map();
|
|
428
|
+
constructor(context) {
|
|
429
|
+
this.context = context;
|
|
430
|
+
}
|
|
431
|
+
components() {
|
|
432
|
+
return (this.#components ??= componentClasses(this.context));
|
|
433
|
+
}
|
|
434
|
+
locate(cls) {
|
|
435
|
+
let located = this.#located.get(cls);
|
|
436
|
+
if (!located) {
|
|
437
|
+
located = locateTemplate(cls);
|
|
438
|
+
this.#located.set(cls, located);
|
|
439
|
+
}
|
|
440
|
+
return located;
|
|
441
|
+
}
|
|
442
|
+
async read(located) {
|
|
443
|
+
const key = located.container === "external" ? located.absolutePath : located;
|
|
444
|
+
let read = this.#reads.get(key);
|
|
445
|
+
if (!read) {
|
|
446
|
+
read = await readTemplate(this.context, located);
|
|
447
|
+
this.#reads.set(key, read);
|
|
448
|
+
}
|
|
449
|
+
return read;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The lines of the file that holds a usage, for context lines. An external template that the
|
|
453
|
+
* scan read is split from that text; any other file (a source file with an inline template) is
|
|
454
|
+
* read from disk once.
|
|
455
|
+
*/
|
|
456
|
+
async lines(absolutePath) {
|
|
457
|
+
let lines = this.#lines.get(absolutePath);
|
|
458
|
+
if (!lines) {
|
|
459
|
+
const read = this.#reads.get(absolutePath);
|
|
460
|
+
const text = read && "text" in read ? read.text : await readFile(absolutePath, "utf8");
|
|
461
|
+
lines = text.split(/\r?\n/);
|
|
462
|
+
this.#lines.set(absolutePath, lines);
|
|
463
|
+
}
|
|
464
|
+
return lines;
|
|
465
|
+
}
|
|
466
|
+
/** The parse of a template returned by {@link read}, errors included. */
|
|
467
|
+
parse(compiler, read, absolutePath) {
|
|
468
|
+
let parsed = this.#parsed.get(read);
|
|
469
|
+
if (!parsed) {
|
|
470
|
+
parsed = compiler.parseTemplate(read.text, absolutePath, { preserveWhitespaces: true });
|
|
471
|
+
this.#parsed.set(read, parsed);
|
|
472
|
+
}
|
|
473
|
+
return parsed;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
434
476
|
/** Reads a located template within the per-template size cap. */
|
|
435
477
|
async function readTemplate(context, located) {
|
|
436
478
|
let text;
|
|
@@ -492,16 +534,11 @@ function bindingTarget(cls, node, name) {
|
|
|
492
534
|
return undefined;
|
|
493
535
|
return { selector, inputs: names.inputs, outputs: names.outputs };
|
|
494
536
|
}
|
|
495
|
-
/** Parses a consumer template, or returns undefined when the compiler reports errors. */
|
|
496
|
-
function parseConsumer(compiler, text, absolutePath) {
|
|
497
|
-
const parsed = compiler.parseTemplate(text, absolutePath, { preserveWhitespaces: true });
|
|
498
|
-
return parsed.errors?.length ? undefined : parsed.nodes;
|
|
499
|
-
}
|
|
500
537
|
function selectorInspector(compiler, target) {
|
|
501
|
-
return (text,
|
|
538
|
+
return (text, parse, tick) => {
|
|
502
539
|
if (!mayMatch(compiler, target.selector, text))
|
|
503
540
|
return "skip";
|
|
504
|
-
const nodes =
|
|
541
|
+
const nodes = parse();
|
|
505
542
|
if (!nodes)
|
|
506
543
|
return "template_parse_error";
|
|
507
544
|
const scan = extractConsumerUsages(compiler, nodes, text, target, tick);
|
|
@@ -514,10 +551,10 @@ function selectorInspector(compiler, target) {
|
|
|
514
551
|
};
|
|
515
552
|
}
|
|
516
553
|
function pipeInspector(compiler, name, rc) {
|
|
517
|
-
return (text,
|
|
554
|
+
return (text, parse, tick) => {
|
|
518
555
|
if (!text.includes(name))
|
|
519
556
|
return "skip";
|
|
520
|
-
const nodes =
|
|
557
|
+
const nodes = parse();
|
|
521
558
|
if (!nodes)
|
|
522
559
|
return "template_parse_error";
|
|
523
560
|
try {
|
|
@@ -536,7 +573,7 @@ function pipeInspector(compiler, name, rc) {
|
|
|
536
573
|
* Scans every component template in the Project with `inspect`. Matching comes first; scope only
|
|
537
574
|
* filters matches. Unreadable templates and unproven scopes are reported.
|
|
538
575
|
*/
|
|
539
|
-
async function scanConsumers(context, cls, inspect, rc, limits) {
|
|
576
|
+
async function scanConsumers(context, cls, compiler, inspect, rc, limits, cache) {
|
|
540
577
|
const remaining = { ...SCAN_LIMITS, ...limits };
|
|
541
578
|
const usages = [];
|
|
542
579
|
let failure;
|
|
@@ -553,25 +590,30 @@ async function scanConsumers(context, cls, inspect, rc, limits) {
|
|
|
553
590
|
rc.checkpoint();
|
|
554
591
|
};
|
|
555
592
|
try {
|
|
556
|
-
for (const consumer of
|
|
593
|
+
for (const consumer of cache.components()) {
|
|
557
594
|
rc.checkpoint();
|
|
558
595
|
if (--remaining.templates < 0)
|
|
559
596
|
throw new WorkExhausted();
|
|
560
|
-
const located =
|
|
597
|
+
const located = cache.locate(consumer);
|
|
561
598
|
if ("reason" in located) {
|
|
562
599
|
failure ??= located.reason;
|
|
563
600
|
continue;
|
|
564
601
|
}
|
|
565
|
-
const read = await
|
|
602
|
+
const read = await cache.read(located);
|
|
566
603
|
if ("reason" in read) {
|
|
567
604
|
failure ??= read.reason;
|
|
568
605
|
continue;
|
|
569
606
|
}
|
|
607
|
+
// The budget is charged for every template, whether or not the cache already held it.
|
|
570
608
|
remaining.bytes -= read.bytes.length;
|
|
571
609
|
if (remaining.bytes < 0)
|
|
572
610
|
throw new WorkExhausted();
|
|
573
611
|
examined += 1;
|
|
574
|
-
const
|
|
612
|
+
const parse = () => {
|
|
613
|
+
const parsed = cache.parse(compiler, read, located.absolutePath);
|
|
614
|
+
return parsed.errors?.length ? undefined : parsed.nodes;
|
|
615
|
+
};
|
|
616
|
+
const scan = inspect(read.text, parse, tick);
|
|
575
617
|
if (scan === "skip")
|
|
576
618
|
continue;
|
|
577
619
|
if (scan === "template_parse_error") {
|
|
@@ -608,7 +650,7 @@ function pipeName(cls) {
|
|
|
608
650
|
return literalString(Node.isPropertyAssignment(property) ? property.getInitializer() : undefined);
|
|
609
651
|
}
|
|
610
652
|
/** `| name` usages of a pipe class in the templates of components that have the class in scope. */
|
|
611
|
-
async function collectPipeUsages(cls, context, rc, limits) {
|
|
653
|
+
async function collectPipeUsages(cls, context, rc, limits, cache) {
|
|
612
654
|
const unfinished = (reason, templates = 0) => ({
|
|
613
655
|
coverage: { status: "unfinished", reason, templates },
|
|
614
656
|
usages: [],
|
|
@@ -619,15 +661,23 @@ async function collectPipeUsages(cls, context, rc, limits) {
|
|
|
619
661
|
const loaded = await loadCompiler(context.tsConfigFilePath);
|
|
620
662
|
if ("reason" in loaded)
|
|
621
663
|
return unfinished(loaded.reason);
|
|
622
|
-
const scan = await scanConsumers(context, cls, pipeInspector(loaded.compiler, name, rc), rc, limits);
|
|
664
|
+
const scan = await scanConsumers(context, cls, loaded.compiler, pipeInspector(loaded.compiler, name, rc), rc, limits, cache);
|
|
623
665
|
const coverage = scan.failure
|
|
624
666
|
? { status: "unfinished", reason: scan.failure, templates: scan.examined }
|
|
625
667
|
: { status: "completed", templates: scan.examined };
|
|
626
668
|
return { coverage, usages: scan.usages };
|
|
627
669
|
}
|
|
628
|
-
/**
|
|
629
|
-
|
|
670
|
+
/**
|
|
671
|
+
* Collects usages of a component member inside its declaring component's template.
|
|
672
|
+
*
|
|
673
|
+
* Pass one `cache` to every collection of a request to share symbol-independent work between
|
|
674
|
+
* them; the result is the same with or without it. By default each call works alone.
|
|
675
|
+
*/
|
|
676
|
+
export async function collectTemplateUsages(node, context, rc, limits = {}, cache = new TemplateScanCache(context)) {
|
|
630
677
|
rc.checkpoint();
|
|
678
|
+
if (cache.context !== context) {
|
|
679
|
+
throw new Error("The template scan cache was created for another ProjectContext.");
|
|
680
|
+
}
|
|
631
681
|
if (!hasAngularCore(context))
|
|
632
682
|
return NOT_APPLICABLE;
|
|
633
683
|
const fail = (status, reason) => ({
|
|
@@ -635,7 +685,7 @@ export async function collectTemplateUsages(node, context, rc, limits = {}) {
|
|
|
635
685
|
usages: [],
|
|
636
686
|
});
|
|
637
687
|
if (Node.isClassDeclaration(node)) {
|
|
638
|
-
return isPipeClass(node) ? collectPipeUsages(node, context, rc, limits) : NOT_APPLICABLE;
|
|
688
|
+
return isPipeClass(node) ? collectPipeUsages(node, context, rc, limits, cache) : NOT_APPLICABLE;
|
|
639
689
|
}
|
|
640
690
|
const owner = memberOwner(node);
|
|
641
691
|
if (!owner)
|
|
@@ -645,14 +695,17 @@ export async function collectTemplateUsages(node, context, rc, limits = {}) {
|
|
|
645
695
|
if (owner.name !== "transform" || componentDecorator(owner.cls)) {
|
|
646
696
|
return fail("unfinished", "cross_component");
|
|
647
697
|
}
|
|
648
|
-
return {
|
|
698
|
+
return {
|
|
699
|
+
...(await collectPipeUsages(owner.cls, context, rc, limits, cache)),
|
|
700
|
+
renameBlocked: true,
|
|
701
|
+
};
|
|
649
702
|
}
|
|
650
703
|
const isComponent = componentDecorator(owner.cls) !== undefined;
|
|
651
704
|
const selectorPublic = isSelectorPublic(node) || isMetadataPublic(owner.cls, owner.name);
|
|
652
705
|
if (!isComponent && selectorPublic && isDirectiveClass(owner.cls)) {
|
|
653
706
|
return fail("unfinished", "cross_component");
|
|
654
707
|
}
|
|
655
|
-
const derived = hasDerivedComponent(
|
|
708
|
+
const derived = hasDerivedComponent(owner.cls, cache);
|
|
656
709
|
if (!isComponent && !derived)
|
|
657
710
|
return NOT_APPLICABLE;
|
|
658
711
|
if (derived)
|
|
@@ -660,16 +713,14 @@ export async function collectTemplateUsages(node, context, rc, limits = {}) {
|
|
|
660
713
|
const loaded = await loadCompiler(context.tsConfigFilePath);
|
|
661
714
|
if ("reason" in loaded)
|
|
662
715
|
return fail("unfinished", loaded.reason);
|
|
663
|
-
const located =
|
|
716
|
+
const located = cache.locate(owner.cls);
|
|
664
717
|
if ("reason" in located)
|
|
665
718
|
return fail("unfinished", located.reason);
|
|
666
|
-
const read = await
|
|
719
|
+
const read = await cache.read(located);
|
|
667
720
|
if ("reason" in read)
|
|
668
721
|
return fail("unfinished", read.reason);
|
|
669
722
|
rc.checkpoint();
|
|
670
|
-
const parsed = loaded.compiler
|
|
671
|
-
preserveWhitespaces: true,
|
|
672
|
-
});
|
|
723
|
+
const parsed = cache.parse(loaded.compiler, read, located.absolutePath);
|
|
673
724
|
if (parsed.errors?.length)
|
|
674
725
|
return fail("unfinished", "template_parse_error");
|
|
675
726
|
let raw;
|
|
@@ -694,7 +745,7 @@ export async function collectTemplateUsages(node, context, rc, limits = {}) {
|
|
|
694
745
|
return unfinished("cross_component");
|
|
695
746
|
if (!loaded.selectors)
|
|
696
747
|
return unfinished("compiler_unsupported");
|
|
697
|
-
const scan = await scanConsumers(context, owner.cls, selectorInspector(loaded.compiler, target), rc, limits);
|
|
748
|
+
const scan = await scanConsumers(context, owner.cls, loaded.compiler, selectorInspector(loaded.compiler, target), rc, limits, cache);
|
|
698
749
|
const coverage = scan.failure
|
|
699
750
|
? { status: "unfinished", reason: scan.failure, templates: scan.examined }
|
|
700
751
|
: { status: "completed", templates: scan.examined };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { TemplateScanCache } from "./angular-templates.js";
|
|
2
3
|
import { buildFileOutline, nodeSourceWithLocation } from "./outline.js";
|
|
3
4
|
import { paginate } from "./pagination.js";
|
|
4
5
|
import { resolveImpactRoot } from "./impact.js";
|
|
5
6
|
import { planCallSpines } from "./call-spines.js";
|
|
6
7
|
import { isCooperativeInterruption, NO_REQUEST_CONTEXT, } from "./request-context.js";
|
|
7
8
|
import { findDeclarationByName, getSourceFileOrThrow, reportSymbolIndexFailure, } from "./project.js";
|
|
8
|
-
import {
|
|
9
|
+
import { collectSymbolReferencesWithTemplateVerdict, } from "./references.js";
|
|
9
10
|
import { collectCompilerCallRelationships } from "./relationships.js";
|
|
10
11
|
import { presentExploreClusters } from "./explore-presentation.js";
|
|
11
12
|
import { searchProjectSymbolsPage, searchProjectSymbolsPageWithIndex, sourceFileSymbols, symbolMatchRank, } from "./symbols.js";
|
|
@@ -151,6 +152,8 @@ export async function buildExploreContext(context, request, requestContext = NO_
|
|
|
151
152
|
const unresolved = [];
|
|
152
153
|
const evidence = [];
|
|
153
154
|
let callSpines;
|
|
155
|
+
// Lives for this request only: the selected symbols share template reads, never a scan budget.
|
|
156
|
+
const templateCache = new TemplateScanCache(effectiveContext);
|
|
154
157
|
if (request.callSpines) {
|
|
155
158
|
if (!request.filePath || !request.symbolPath || routed.route !== "symbol") {
|
|
156
159
|
throw new Error("Call spines require an exact file_path and symbol_path.");
|
|
@@ -197,7 +200,13 @@ export async function buildExploreContext(context, request, requestContext = NO_
|
|
|
197
200
|
}
|
|
198
201
|
if (expansion.references) {
|
|
199
202
|
try {
|
|
200
|
-
references =
|
|
203
|
+
references = await collectSymbolReferencesWithTemplateVerdict(node, effectiveContext, {
|
|
204
|
+
symbolPath: record.selector,
|
|
205
|
+
includeDeclaration: true,
|
|
206
|
+
detail: request.referenceDetail,
|
|
207
|
+
offset: 0,
|
|
208
|
+
limit: request.referenceLimit,
|
|
209
|
+
}, requestContext, templateCache);
|
|
201
210
|
}
|
|
202
211
|
catch (error) {
|
|
203
212
|
if (isCooperativeInterruption(error))
|
|
@@ -231,6 +240,16 @@ export async function buildExploreContext(context, request, requestContext = NO_
|
|
|
231
240
|
reason: "reference_limit",
|
|
232
241
|
});
|
|
233
242
|
}
|
|
243
|
+
// Absent coverage means not applicable; anything short of completed leaves usages unknown.
|
|
244
|
+
const coverage = item.references?.template_coverage?.status;
|
|
245
|
+
if (coverage !== undefined && coverage !== "completed" && coverage !== "not_applicable") {
|
|
246
|
+
omissions.push({
|
|
247
|
+
subject: item.selector,
|
|
248
|
+
category: "incomplete",
|
|
249
|
+
component: "references",
|
|
250
|
+
reason: "template_coverage_incomplete",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
234
253
|
}
|
|
235
254
|
if (callSpines?.authority_state === "untrusted") {
|
|
236
255
|
omissions.push({
|
|
@@ -38,7 +38,7 @@ export class H05LifecycleFixtureController {
|
|
|
38
38
|
const generationSignal = this.#generationAbort.signal, signal = AbortSignal.any([context.signal, generationSignal]);
|
|
39
39
|
const checkpoint = () => { if (!this.accepts(identity) || generationSignal.aborted)
|
|
40
40
|
throw new H03TimeoutFixtureError(); context.checkpoint(); };
|
|
41
|
-
const hooked = Object.freeze({ get sequence() { return context.sequence; }, get phase() { return context.phase; }, signal, checkpoint, markExecuting: () => context.markExecuting(), enterCompletionCritical: () => { checkpoint(); context.enterCompletionCritical(); } });
|
|
41
|
+
const hooked = Object.freeze({ get sequence() { return context.sequence; }, get phase() { return context.phase; }, signal, checkpoint, markExecuting: () => context.markExecuting(), remainingMs: () => context.remainingMs(), enterCompletionCritical: () => { checkpoint(); context.enterCompletionCritical(); } });
|
|
42
42
|
this.#active += 1;
|
|
43
43
|
this.#emit({ ...eventIdentity, phase: "started" });
|
|
44
44
|
try {
|
|
@@ -201,6 +201,7 @@ export class H03TimeoutFixtureController {
|
|
|
201
201
|
signal,
|
|
202
202
|
checkpoint,
|
|
203
203
|
markExecuting: () => context.markExecuting(),
|
|
204
|
+
remainingMs: () => context.remainingMs(),
|
|
204
205
|
enterCompletionCritical: () => {
|
|
205
206
|
checkpoint();
|
|
206
207
|
context.enterCompletionCritical();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const REVIEW_HINT = "Prepare again with allow_new_errors=true only after explicit review.";
|
|
2
|
+
/**
|
|
3
|
+
* A plan is blocked by new TypeScript errors, and, only when opted in, by new template errors of
|
|
4
|
+
* a `completed` check. A check that did not complete never blocks. `allow_new_errors` waives both.
|
|
5
|
+
*/
|
|
6
|
+
export function evaluateBlock(input) {
|
|
7
|
+
const templateErrors = input.blockOnTemplateErrors && input.template?.status === "completed"
|
|
8
|
+
? input.template.addedErrors.length
|
|
9
|
+
: 0;
|
|
10
|
+
const reasons = [
|
|
11
|
+
...(input.typescriptErrors > 0
|
|
12
|
+
? [`${input.typescriptErrors} new TypeScript error(s) would be introduced.`]
|
|
13
|
+
: []),
|
|
14
|
+
...(templateErrors > 0 ? [`${templateErrors} new template error(s) would be introduced.`] : []),
|
|
15
|
+
];
|
|
16
|
+
if (input.allowNewErrors || reasons.length === 0)
|
|
17
|
+
return { blocked: false, reason: null };
|
|
18
|
+
return { blocked: true, reason: `${reasons.join(" ")} ${REVIEW_HINT}` };
|
|
19
|
+
}
|
|
@@ -6,6 +6,8 @@ import { z } from "zod";
|
|
|
6
6
|
import { applyOperation, configureOperationApply, exportOperationRecord, importOperationRecord, } from "./operations.js";
|
|
7
7
|
import { ensurePrivateDirectory, resolveRuntimeStateDirectory, } from "./runtime-state.js";
|
|
8
8
|
export const PLAN_SCHEMA_VERSION = 2;
|
|
9
|
+
/** Plans that carry a template type-check delta persist as this version. */
|
|
10
|
+
export const TEMPLATE_PLAN_SCHEMA_VERSION = 3;
|
|
9
11
|
export const MAX_PLAN_BYTES = 50 * 1024 * 1024;
|
|
10
12
|
const HashSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
11
13
|
const IsoDateSchema = z.string().datetime({ offset: true });
|
|
@@ -38,8 +40,27 @@ const PersistedFileSchema = AffectedFileSchema.extend({
|
|
|
38
40
|
updated_bytes_base64: z.string(),
|
|
39
41
|
mode: z.number().int().nonnegative(),
|
|
40
42
|
}).strict();
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
+
const TemplateReasonSchema = z.enum([
|
|
44
|
+
"compiler_cli_missing",
|
|
45
|
+
"compiler_unsupported",
|
|
46
|
+
"typescript_incompatible",
|
|
47
|
+
"work_exhausted",
|
|
48
|
+
"deadline",
|
|
49
|
+
"check_failed",
|
|
50
|
+
]);
|
|
51
|
+
const TemplateDiagnosticsSchema = z.discriminatedUnion("status", [
|
|
52
|
+
DiagnosticDeltaSchema.extend({ status: z.literal("completed") }).strict(),
|
|
53
|
+
z
|
|
54
|
+
.object({
|
|
55
|
+
status: z.enum(["unavailable", "unfinished"]),
|
|
56
|
+
reason: TemplateReasonSchema,
|
|
57
|
+
added: z.array(DiagnosticSchema).max(0),
|
|
58
|
+
removed: z.array(DiagnosticSchema).max(0),
|
|
59
|
+
addedErrors: z.array(DiagnosticSchema).max(0),
|
|
60
|
+
})
|
|
61
|
+
.strict(),
|
|
62
|
+
]);
|
|
63
|
+
const PersistedOperationShape = {
|
|
43
64
|
operation_id: z.string().uuid(),
|
|
44
65
|
plan_hash: HashSchema,
|
|
45
66
|
kind: z.enum(["rename_symbol", "replace_symbol_body", "scaffold_class"]),
|
|
@@ -60,14 +81,24 @@ const PersistedOperationSchema = z
|
|
|
60
81
|
preview_truncated: z.boolean(),
|
|
61
82
|
applied_at: IsoDateSchema.optional(),
|
|
62
83
|
files: z.array(PersistedFileSchema).min(1),
|
|
84
|
+
};
|
|
85
|
+
const PersistedOperationSchema = z.object(PersistedOperationShape).strict();
|
|
86
|
+
const PersistedOperationV3Schema = z
|
|
87
|
+
.object({
|
|
88
|
+
...PersistedOperationShape,
|
|
89
|
+
template_diagnostics: TemplateDiagnosticsSchema,
|
|
90
|
+
block_on_template_errors: z.boolean(),
|
|
63
91
|
})
|
|
64
92
|
.strict();
|
|
65
93
|
const PlanEnvelopeSchema = z.discriminatedUnion("schema_version", [
|
|
66
94
|
z.object({ schema_version: z.literal(1), operation: PersistedOperationSchema }).strict(),
|
|
67
95
|
z.object({ schema_version: z.literal(2), operation: PersistedOperationSchema }).strict(),
|
|
96
|
+
z.object({ schema_version: z.literal(3), operation: PersistedOperationV3Schema }).strict(),
|
|
68
97
|
]);
|
|
69
98
|
function planEnvelope(version, operation) {
|
|
70
|
-
|
|
99
|
+
if (version === 1)
|
|
100
|
+
return { schema_version: 1, operation };
|
|
101
|
+
return version === 2 ? { schema_version: 2, operation } : { schema_version: 3, operation };
|
|
71
102
|
}
|
|
72
103
|
function stateDirectory(options) {
|
|
73
104
|
return resolveRuntimeStateDirectory(options);
|
|
@@ -148,7 +179,10 @@ async function readPlanEnvelope(planFileInput) {
|
|
|
148
179
|
export async function persistOperationPlan(operationId, options = {}) {
|
|
149
180
|
const plansDirectory = path.join(stateDirectory(options), "plans");
|
|
150
181
|
const planFile = path.join(plansDirectory, `${operationId}.astplan`);
|
|
151
|
-
|
|
182
|
+
const record = exportOperationRecord(operationId);
|
|
183
|
+
await atomicWritePlan(planFile, planEnvelope(record.template_diagnostics === undefined
|
|
184
|
+
? PLAN_SCHEMA_VERSION
|
|
185
|
+
: TEMPLATE_PLAN_SCHEMA_VERSION, record));
|
|
152
186
|
return planFile;
|
|
153
187
|
}
|
|
154
188
|
export async function applyPersistedOperation(planFileInput, planHash, options = {}) {
|