@azure-id/orc 1.8.2 → 1.9.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/CHANGELOG.md +299 -0
- package/README-id.md +68 -51
- package/README.md +60 -45
- package/bin/cli.js +301 -39
- package/bin/graph-audit.js +235 -0
- package/bin/graph-extract.js +308 -3
- package/bin/graph-gain.js +23 -3
- package/bin/graph-map.js +9 -1
- package/bin/graph-query.js +152 -10
- package/bin/graph-shard.js +6 -1
- package/bin/graph-signals.js +287 -4
- package/bin/graph.js +717 -658
- package/bin/verify-contracts.js +277 -61
- package/bin/verify-package.js +19 -1
- package/bin/webui/fixtures/knowledge.js +21 -4
- package/bin/webui/i18n/en/knowledge.json +5 -2
- package/bin/webui/i18n/id/knowledge.json +5 -2
- package/bin/webui/js/panels/knowledge.js +21 -2
- package/mock-run/orc-quick.md +141 -113
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +15 -5
- package/templates/agents/orc-executor-haiku-4-5.md +19 -1
- package/templates/agents/orc-executor-opus-4-7-high.md +19 -1
- package/templates/agents/orc-executor-opus-4-7-med.md +19 -1
- package/templates/agents/orc-executor-opus-4-8-high.md +19 -1
- package/templates/agents/orc-executor-opus-5-high.md +19 -1
- package/templates/agents/orc-executor-opus-5-low.md +19 -1
- package/templates/agents/orc-executor-opus-5-med.md +19 -1
- package/templates/agents/orc-executor-sonnet-4-6-high.md +19 -1
- package/templates/agents/orc-executor-sonnet-4-6-med.md +19 -1
- package/templates/agents/orc-executor-sonnet-5-high.md +19 -1
- package/templates/agents/orc-planner-mini-opus-5-med.md +75 -69
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +73 -67
- package/templates/agents/orc-recon-opus-5-low.md +109 -0
- package/templates/agents/orc-recon-sonnet-4-6-med.md +109 -0
- package/templates/commands/orc-mini.md +10 -12
- package/templates/commands/orc-quick.md +20 -33
- package/templates/hooks/README.md +11 -1
- package/templates/hooks/orc-graph-hook.js +53 -7
- package/templates/hooks/orc-trace.js +476 -471
- package/templates/skills/_shared/code-graph.md +56 -17
- package/templates/skills/_shared/phases/execution.md +4 -4
- package/templates/skills/_shared/phases/planning.md +3 -3
- package/templates/skills/_shared/phases/preflight.md +1 -1
- package/templates/skills/_shared/phases/review.md +3 -1
- package/templates/skills/_shared/phases/rules.md +172 -159
- package/templates/skills/_shared/phases/ship.md +1 -1
- package/templates/skills/_shared/phases/trace.md +4 -2
- package/templates/skills/_shared/phases/wiki-consult.md +9 -5
- package/templates/skills/_shared/read-ladder.md +14 -1
- package/templates/skills/_shared/return-validation.md +22 -0
- package/templates/skills/context-combiner/SKILL.md +13 -13
- package/templates/skills/orc/SKILL.md +1 -1
- package/templates/skills/orc/subskills/orc-execution/core.md +171 -159
- package/templates/skills/orc-fast/SKILL.md +3 -3
- package/templates/skills/orc-mini/SKILL.md +145 -137
- package/templates/skills/orc-mini/examples/mini-run-mock.md +64 -50
- package/templates/skills/orc-mini/references/complexity.md +119 -0
- package/templates/skills/orc-quick/README.md +495 -423
- package/templates/skills/orc-quick/SKILL.md +157 -212
- package/templates/skills/orc-quick/references/context-doc.md +145 -114
- package/templates/skills/orc-quick/references/defect.md +101 -0
- package/templates/skills/orc-quick/references/dispatch-gate.md +55 -24
- package/templates/skills/orc-quick/references/gh-mode.md +148 -127
- package/templates/skills/orc-quick/references/look.md +107 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ── orc graph audit (v1.9.1 A2) — WHY the map is thin ───────────────────────
|
|
3
|
+
//
|
|
4
|
+
// `orc graph status` can now say THIN. This says why, and it is the only
|
|
5
|
+
// answer in the family whose subject is the PARSER rather than the code.
|
|
6
|
+
//
|
|
7
|
+
// The question it exists for is a real one, measured on a real project: a graph
|
|
8
|
+
// with 1.9 symbols per file answers `exit 4` to most questions, and until now
|
|
9
|
+
// nothing told anyone whether the code has no named functions or the parser did
|
|
10
|
+
// not read them. Those are opposite problems with opposite fixes, and the only
|
|
11
|
+
// evidence that tells them apart is what the empty files actually LOOK like.
|
|
12
|
+
//
|
|
13
|
+
// THE RULES THIS FILE HOLDS
|
|
14
|
+
//
|
|
15
|
+
// 1. It READS. It never writes a file, never takes the lock, never runs the
|
|
16
|
+
// extractor, and it costs zero model tokens.
|
|
17
|
+
// 2. A zero-symbol file is NOT a defect by itself. A component with no named
|
|
18
|
+
// function is a file the parser read correctly. The last line says so, and
|
|
19
|
+
// the shape reading is a HINT that never becomes a verdict.
|
|
20
|
+
// 3. It reads at most `SHAPE_LINES` lines of a file, and only of a file it is
|
|
21
|
+
// about to name. A repository of ten thousand empty files is still one
|
|
22
|
+
// bounded read per LISTED file.
|
|
23
|
+
// 4. It is a USER command. `LANE_CALLS["graph-audit"].lanes` is `[]` and a
|
|
24
|
+
// test holds it there: nothing a lane does is made better by an audit.
|
|
25
|
+
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const path = require("path");
|
|
28
|
+
const G = require("./graph.js");
|
|
29
|
+
|
|
30
|
+
// How far into a file the sampler looks for its first real declaration. A file
|
|
31
|
+
// with a 40-line licence header and then `export default {` is common; a file
|
|
32
|
+
// whose first declaration is below line 40 is rare enough to read as `other`.
|
|
33
|
+
const SHAPE_LINES = 40;
|
|
34
|
+
const TOP_DEFAULT = 20;
|
|
35
|
+
const TOP_CAP = 200;
|
|
36
|
+
|
|
37
|
+
// The languages whose empty files are worth sampling. Sampling a `.json`
|
|
38
|
+
// fixture would produce a shape nobody can act on.
|
|
39
|
+
const SAMPLED = new Set(["js", "ts", "vue", "svelte"]);
|
|
40
|
+
|
|
41
|
+
// Each shape, the plain reading that goes beside it, and whether that reading
|
|
42
|
+
// is a PARSER GAP — a file the parser should have indexed and did not. Only
|
|
43
|
+
// those three are worth an issue; the rest are files that really are data.
|
|
44
|
+
const SHAPES = [
|
|
45
|
+
{ id: "script-setup", test: /^<script\s+setup/, reading: "a component with no named function — the parser is right, there is nothing to name", gap: false },
|
|
46
|
+
{ id: "script", test: /^<script[\s>]/, reading: "a component whose script block declared nothing the parser names", gap: true },
|
|
47
|
+
// graph@6 (W5b) names the members of a component, a mixin and a store
|
|
48
|
+
// module, and the plain keys of a data object. An object that is STILL empty
|
|
49
|
+
// has neither — quoted keys, or nothing at all.
|
|
50
|
+
{ id: "export-default-object", test: /^export default \{/, reading: "an object literal with no member the parser names (quoted keys, or empty) — data, not code", gap: false },
|
|
51
|
+
{ id: "export-default-component", test: /^export default (defineComponent|Vue\.extend)\s*\(/, reading: "a component defined as an object whose members the parser did not find — a parser gap; report it with one file path", gap: true },
|
|
52
|
+
{ id: "export-default-class", test: /^export default class\b/, reading: "a class the parser should index — report it with one file path", gap: true },
|
|
53
|
+
{ id: "export-const-fn", test: /^export const \w+\s*=\s*(\(|async\b)/, reading: "an arrow constant the parser SHOULD index — a parser gap; report it with one file path", gap: true },
|
|
54
|
+
{ id: "export-function", test: /^export (async )?function\b/, reading: "a function the parser should index — report it with one file path", gap: true },
|
|
55
|
+
{ id: "module-exports-object", test: /^module\.exports\s*=\s*\{/, reading: "data", gap: false },
|
|
56
|
+
{ id: "class", test: /^class \w+/, reading: "a class the parser should index — report it with one file path", gap: true },
|
|
57
|
+
{ id: "function", test: /^(async )?function \w+/, reading: "a function the parser should index — report it with one file path", gap: true },
|
|
58
|
+
];
|
|
59
|
+
const OTHER = { id: "other", reading: "no declaration in the first lines — data, a type file, or a re-export", gap: false };
|
|
60
|
+
|
|
61
|
+
const readJson = (f) => {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(f, "utf8"));
|
|
64
|
+
} catch (_) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// The first line of a file that is not blank, not a comment and not an import.
|
|
70
|
+
// The import skip matters: a Vue SFC opens with twenty imports and its shape is
|
|
71
|
+
// whatever follows them.
|
|
72
|
+
function firstDeclaration(abs) {
|
|
73
|
+
let text;
|
|
74
|
+
try {
|
|
75
|
+
text = fs.readFileSync(abs, "utf8");
|
|
76
|
+
} catch (_) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const lines = text.split(/\r?\n/).slice(0, SHAPE_LINES);
|
|
80
|
+
let inBlock = false;
|
|
81
|
+
for (const raw of lines) {
|
|
82
|
+
const line = raw.trim();
|
|
83
|
+
if (!line) continue;
|
|
84
|
+
if (inBlock) {
|
|
85
|
+
if (line.includes("*/")) inBlock = false;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (line.startsWith("/*")) {
|
|
89
|
+
if (!line.includes("*/")) inBlock = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (line.startsWith("//") || line.startsWith("*") || line.startsWith("<!--")) continue;
|
|
93
|
+
// A directive prologue is not a declaration, and neither is an import. Skip
|
|
94
|
+
// both, or every CommonJS file in the repository reads as `other`.
|
|
95
|
+
if (/^["']use (strict|client|server)["'];?$/.test(line)) continue;
|
|
96
|
+
if (/^(import|export \{|export \*|const .* = require\()/.test(line)) continue;
|
|
97
|
+
return line;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function shapeOf(abs) {
|
|
103
|
+
const line = firstDeclaration(abs);
|
|
104
|
+
if (line == null) return OTHER;
|
|
105
|
+
for (const s of SHAPES) if (s.test.test(line)) return s;
|
|
106
|
+
return OTHER;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function audit(claudeDir, root, opts) {
|
|
110
|
+
const o = opts || {};
|
|
111
|
+
const p = G.graphPaths(claudeDir);
|
|
112
|
+
const index = readJson(p.index);
|
|
113
|
+
const files = readJson(p.files);
|
|
114
|
+
const meta = readJson(p.meta);
|
|
115
|
+
if (!index || !files || !meta) return { ok: false, state: "none", reason: "no-index", exit: 1 };
|
|
116
|
+
|
|
117
|
+
const SIG = require("./graph-signals.js");
|
|
118
|
+
const density = meta.density || G.densityOf(index, files);
|
|
119
|
+
const thin = SIG.isThin(density, meta.files || Object.keys(index.by_file || {}).length);
|
|
120
|
+
const top = Math.max(1, Math.min(TOP_CAP, Number(o.top) || TOP_DEFAULT));
|
|
121
|
+
|
|
122
|
+
// Per language: the density field, plus which extractor actually read each
|
|
123
|
+
// file. A language read by the heuristic where a real parser was expected is
|
|
124
|
+
// half the answer on its own.
|
|
125
|
+
const byLang = {};
|
|
126
|
+
for (const [lang, v] of Object.entries(density.by_lang || {})) byLang[lang] = { ...v, per_file: v.files ? Math.round((10 * v.symbols) / v.files) / 10 : 0, extractor: {} };
|
|
127
|
+
for (const [rel, v] of Object.entries(files)) {
|
|
128
|
+
const lang = (index.by_file[rel] && index.by_file[rel].lang) || G.LANG_BY_EXT[path.extname(rel).toLowerCase()] || "other";
|
|
129
|
+
const row = byLang[lang];
|
|
130
|
+
if (!row || !v || !v.extractor) continue;
|
|
131
|
+
row.extractor[v.extractor] = (row.extractor[v.extractor] || 0) + 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The empty files, longest first: the longest file the parser read as empty
|
|
135
|
+
// is the one that says most about what it cannot read.
|
|
136
|
+
const zero = [];
|
|
137
|
+
const partial = [];
|
|
138
|
+
for (const [rel, v] of Object.entries(index.by_file)) {
|
|
139
|
+
const n = (v.symbols || []).filter((s) => s.kind !== "module").length;
|
|
140
|
+
if (v.coverage === "partial") partial.push({ path: rel, ranges: v.partial || [] });
|
|
141
|
+
if (n > 0) continue;
|
|
142
|
+
zero.push({ path: rel, lines: Number(v.lines) || 0, lang: v.lang || G.LANG_BY_EXT[path.extname(rel).toLowerCase()] || "other" });
|
|
143
|
+
}
|
|
144
|
+
zero.sort((a, b) => b.lines - a.lines || a.path.localeCompare(b.path));
|
|
145
|
+
|
|
146
|
+
// The sampler reads only the files it lists, and only the languages whose
|
|
147
|
+
// shape means anything.
|
|
148
|
+
const shapeCount = new Map();
|
|
149
|
+
for (const row of zero) {
|
|
150
|
+
if (!SAMPLED.has(row.lang)) continue;
|
|
151
|
+
const s = shapeOf(path.join(root, ...row.path.split("/")));
|
|
152
|
+
row.shape = s.id;
|
|
153
|
+
shapeCount.set(s.id, (shapeCount.get(s.id) || 0) + 1);
|
|
154
|
+
}
|
|
155
|
+
const shapes = [...shapeCount.entries()]
|
|
156
|
+
.map(([id, count]) => {
|
|
157
|
+
const def = SHAPES.find((x) => x.id === id) || OTHER;
|
|
158
|
+
return { shape: id, count, reading: def.reading, gap: def.gap };
|
|
159
|
+
})
|
|
160
|
+
.sort((a, b) => b.count - a.count || a.shape.localeCompare(b.shape));
|
|
161
|
+
|
|
162
|
+
const skipped = Object.entries(files)
|
|
163
|
+
.filter(([, v]) => v && v.skipped)
|
|
164
|
+
.map(([rel, v]) => ({ path: rel, reason: v.skipped, bytes: Number(v.bytes) || 0 }))
|
|
165
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
166
|
+
|
|
167
|
+
const gen = meta.generation || 0;
|
|
168
|
+
const num = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
169
|
+
const pad = (v, w) => String(v).padStart(w);
|
|
170
|
+
const head =
|
|
171
|
+
`graph audit (gen ${gen}) — ${num(meta.files || 0)} files · ${num(meta.symbols || 0)} symbols · ` +
|
|
172
|
+
`${density.symbols_per_file} symbols/file${thin ? " · THIN" : ""}`;
|
|
173
|
+
const table = [
|
|
174
|
+
" lang files symbols per-file zero partial skipped extractor",
|
|
175
|
+
...Object.entries(byLang)
|
|
176
|
+
.sort((a, b) => b[1].files - a[1].files || a[0].localeCompare(b[0]))
|
|
177
|
+
.map(([lang, v]) => {
|
|
178
|
+
const ex = Object.entries(v.extractor)
|
|
179
|
+
.sort((a, b) => b[1] - a[1])
|
|
180
|
+
.map(([k, c]) => `${k} (${c})`)
|
|
181
|
+
.join(" · ");
|
|
182
|
+
return ` ${lang.padEnd(6)} ${pad(num(v.files), 5)} ${pad(num(v.symbols), 8)} ${pad(v.per_file, 9)} ${pad(num(v.zero), 5)} ${pad(num(v.partial), 9)} ${pad(num(v.skipped), 8)} ${ex}`;
|
|
183
|
+
}),
|
|
184
|
+
];
|
|
185
|
+
const zeroList = zero.length
|
|
186
|
+
? [
|
|
187
|
+
` files the parser read as EMPTY, longest first (${Math.min(top, zero.length)} of ${num(zero.length)}${zero.length > top ? " · --top=N for more" : ""})`,
|
|
188
|
+
...zero.slice(0, top).map((z) => ` ${pad(num(z.lines), 6)} ${z.path}${z.shape ? ` ${z.shape}` : ""}`),
|
|
189
|
+
]
|
|
190
|
+
: [" no file was read as empty"];
|
|
191
|
+
const shapeList = shapes.length
|
|
192
|
+
? [
|
|
193
|
+
` what the first declaration in an empty file looks like (${num(zero.filter((z) => SAMPLED.has(z.lang)).length)} files sampled)`,
|
|
194
|
+
...shapes.map((s) => ` ${s.shape.padEnd(26)} ${pad(num(s.count), 5)} ${s.reading}`),
|
|
195
|
+
]
|
|
196
|
+
: [];
|
|
197
|
+
const skippedList = skipped.length
|
|
198
|
+
? [` skipped (${skipped.length})`, ...skipped.slice(0, top).map((x) => ` ${x.reason.padEnd(11)} ${x.path}${x.bytes ? ` (${Math.round(x.bytes / 1024)} KB)` : ""}`)]
|
|
199
|
+
: [];
|
|
200
|
+
const partialList = partial.length
|
|
201
|
+
? [` partial (${partial.length}) ` + partial.slice(0, 6).map((x) => `${x.path} ${(x.ranges[0] || []).join("-")}`).join(" · ") + (partial.length > 6 ? " · … (--json lists all)" : "")]
|
|
202
|
+
: [];
|
|
203
|
+
|
|
204
|
+
const line = [
|
|
205
|
+
head,
|
|
206
|
+
...table,
|
|
207
|
+
...zeroList,
|
|
208
|
+
...shapeList,
|
|
209
|
+
...skippedList,
|
|
210
|
+
...partialList,
|
|
211
|
+
" an empty file is not a defect by itself — the graph names functions, methods, classes and routes;",
|
|
212
|
+
' a shape marked "parser gap" is worth one issue with the file path',
|
|
213
|
+
].join("\n");
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
state: "found",
|
|
218
|
+
generation: gen,
|
|
219
|
+
gen_id: meta.gen_id || null,
|
|
220
|
+
density,
|
|
221
|
+
thin,
|
|
222
|
+
top,
|
|
223
|
+
by_lang: byLang,
|
|
224
|
+
zero_files: zero.slice(0, top),
|
|
225
|
+
total_zero: zero.length,
|
|
226
|
+
shapes,
|
|
227
|
+
skipped,
|
|
228
|
+
partial,
|
|
229
|
+
line,
|
|
230
|
+
trace: `GRAPH-AUDIT ${thin ? "thin" : "ok"} :: files=${meta.files || 0} symbols=${meta.symbols || 0} per_file=${density.symbols_per_file} zero=${zero.length} skipped=${skipped.length} gen=${gen}`,
|
|
231
|
+
exit: 0,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = { audit, shapeOf, firstDeclaration, SHAPES, OTHER, SHAPE_LINES, TOP_DEFAULT, TOP_CAP };
|
package/bin/graph-extract.js
CHANGED
|
@@ -56,7 +56,7 @@ const { spawnSync } = require("child_process");
|
|
|
56
56
|
// @3 (W9): route handlers are symbols, and a function passed BY NAME is a `ref`.
|
|
57
57
|
// @4 (EW1): every record says how much of the file the extractor actually saw.
|
|
58
58
|
// @5 (v1.8.2 W1): urls, mounts, decorator routes, `handler` on a route alias.
|
|
59
|
-
const HEURISTIC = "heuristic@
|
|
59
|
+
const HEURISTIC = "heuristic@6";
|
|
60
60
|
const MAX_CALLS_PER_SYMBOL = 200;
|
|
61
61
|
const MAX_EFFECTS_PER_SYMBOL = 12;
|
|
62
62
|
const MAX_URLS_PER_SYMBOL = 200;
|
|
@@ -1061,6 +1061,288 @@ function basesOf(tail) {
|
|
|
1061
1061
|
return [...new Set(out)].slice(0, 8);
|
|
1062
1062
|
}
|
|
1063
1063
|
|
|
1064
|
+
// ── W5b (v1.9.1, DE-15): an OBJECT is a declaration too ─────────────────────
|
|
1065
|
+
// An Options API component, a mixin and a Vuex module define every function
|
|
1066
|
+
// they have as a MEMBER of one default-exported object. The rungs above read
|
|
1067
|
+
// declarations at a line start, so on a Vue project 416 of 429 `.vue` files
|
|
1068
|
+
// had no symbol at all. This pass runs on the masked source for js/ts (and an
|
|
1069
|
+
// SFC's `<script>`), on the heuristic rung AND after the borrowed TypeScript
|
|
1070
|
+
// walk — the walker only records a method inside a class, so both were blind in
|
|
1071
|
+
// the same place.
|
|
1072
|
+
//
|
|
1073
|
+
// It adds no new idea to the resolver: the object is a `class`, a member is a
|
|
1074
|
+
// `method` named `<Owner>.<name>`, `mixins`/`extends` are its `bases`. So
|
|
1075
|
+
// `this.submit()` resolves LOCAL through `classOf`, and a mixin member is
|
|
1076
|
+
// reached through `inheritedMember`, exactly as a class method is.
|
|
1077
|
+
const OBJ_SECTIONS = new Set(["methods", "computed", "watch", "getters", "mutations", "actions", "filters", "provide"]);
|
|
1078
|
+
const DEFAULT_OBJECT = /\bexport[ \t]+default[ \t]+(?:(?:defineComponent|Vue[ \t]*\.[ \t]*extend)[ \t]*\([ \t\r\n]*)?\{/g;
|
|
1079
|
+
const IDENT_RE = /^[A-Za-z_$][\w$]*$/;
|
|
1080
|
+
// A value that is a function: `function (…)`, `async function`, `(…) =>`,
|
|
1081
|
+
// `x =>`. The arrow's parameter list may span lines, so it is matched by pair.
|
|
1082
|
+
function fnValueAt(m, v) {
|
|
1083
|
+
const rest = m.slice(v, v + 200);
|
|
1084
|
+
if (/^(?:async[ \t]+)?function\b/.test(rest)) {
|
|
1085
|
+
const p = m.indexOf("(", v);
|
|
1086
|
+
return { paren: p };
|
|
1087
|
+
}
|
|
1088
|
+
if (/^(?:async[ \t]+)?[A-Za-z_$][\w$]*[ \t]*=>/.test(rest) && !/^(?:async[ \t]+)?(?:function|class|new)\b/.test(rest)) return { paren: null };
|
|
1089
|
+
const a = /^(?:async[ \t]*)?\(/.exec(rest);
|
|
1090
|
+
if (a) {
|
|
1091
|
+
const p = v + a[0].length - 1;
|
|
1092
|
+
const pc = matchPairQuiet(m, p, "(", ")");
|
|
1093
|
+
if (pc > 0 && /^\s*(?::[^=\n]+)?=>/.test(m.slice(pc + 1, pc + 200))) return { paren: null };
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// `matchPair` records an unclosed `{` as a coverage gap. A speculative probe
|
|
1099
|
+
// for an arrow's parameter list must never do that.
|
|
1100
|
+
function matchPairQuiet(s, open, a, b) {
|
|
1101
|
+
let d = 0;
|
|
1102
|
+
for (let k = open; k < s.length; k++) {
|
|
1103
|
+
if (s[k] === a) d++;
|
|
1104
|
+
else if (s[k] === b && --d === 0) return k;
|
|
1105
|
+
}
|
|
1106
|
+
return -1;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// The depth-0 members of an object literal, as [start, end) with the leading
|
|
1110
|
+
// whitespace dropped. The mask keeps quotes and blanks what is inside them, so
|
|
1111
|
+
// a comma in a string can never split a member.
|
|
1112
|
+
function objectMembers(m, open, close) {
|
|
1113
|
+
const out = [];
|
|
1114
|
+
let d = 0;
|
|
1115
|
+
let a = open + 1;
|
|
1116
|
+
for (let k = open + 1; k < close; k++) {
|
|
1117
|
+
const ch = m[k];
|
|
1118
|
+
if (ch === "(" || ch === "[" || ch === "{") d++;
|
|
1119
|
+
else if (ch === ")" || ch === "]" || ch === "}") d--;
|
|
1120
|
+
else if (ch === "," && d === 0) {
|
|
1121
|
+
out.push([a, k]);
|
|
1122
|
+
a = k + 1;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
out.push([a, close]);
|
|
1126
|
+
const kept = [];
|
|
1127
|
+
for (let [x, y] of out) {
|
|
1128
|
+
while (x < y && /\s/.test(m[x])) x++;
|
|
1129
|
+
if (x < y) kept.push([x, y]);
|
|
1130
|
+
}
|
|
1131
|
+
return kept;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const KEY = "([A-Za-z_$][\\w$]*|\\[[ \\t]*[A-Za-z_$][\\w$.]*[ \\t]*\\])";
|
|
1135
|
+
const MEM_ACCESSOR = /^(?:get|set)[ \t]+([A-Za-z_$][\w$]*)[ \t]*\(/;
|
|
1136
|
+
const MEM_METHOD = new RegExp(`^(?:async[ \\t]+)?\\*?[ \\t]*${KEY}[ \\t]*\\(`);
|
|
1137
|
+
const MEM_PAIR = new RegExp(`^${KEY}[ \\t]*:[ \\t\\r\\n]*`);
|
|
1138
|
+
const keyName = (k) => (k.startsWith("[") ? k.replace(/[[\]\s]/g, "").split(".").pop() : k);
|
|
1139
|
+
|
|
1140
|
+
// What one member IS. `fn` carries the paren of a method shorthand (so the
|
|
1141
|
+
// call scanner never reads `submit() {` as a call to `submit`).
|
|
1142
|
+
function memberOf(m, a, b) {
|
|
1143
|
+
const seg = m.slice(a, b);
|
|
1144
|
+
if (seg.startsWith("...")) return null;
|
|
1145
|
+
let x = MEM_ACCESSOR.exec(seg);
|
|
1146
|
+
if (x) return { type: "fn", name: x[1], paren: a + x[0].length - 1 };
|
|
1147
|
+
x = MEM_METHOD.exec(seg);
|
|
1148
|
+
if (x && !/^(?:function|async)$/.test(x[1])) return { type: "fn", name: keyName(x[1]), paren: a + x[0].length - 1 };
|
|
1149
|
+
x = MEM_PAIR.exec(seg);
|
|
1150
|
+
if (x) {
|
|
1151
|
+
const name = keyName(x[1]);
|
|
1152
|
+
const v = a + x[0].length;
|
|
1153
|
+
const f = fnValueAt(m, v);
|
|
1154
|
+
// `submit: function submit() {}` — the paren is a declaration's, never a call.
|
|
1155
|
+
if (f) return { type: "fn", name, paren: f.paren, v };
|
|
1156
|
+
if (m[v] === "{") return { type: "obj", name, open: v, close: matchPairQuiet(m, v, "{", "}"), v };
|
|
1157
|
+
if (m[v] === "[") return { type: "arr", name, open: v, close: matchPairQuiet(m, v, "[", "]"), v };
|
|
1158
|
+
return { type: "val", name, v };
|
|
1159
|
+
}
|
|
1160
|
+
if (/^[A-Za-z_$][\w$]*\s*$/.test(seg)) return { type: "short", name: seg.trim() };
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// The owner's name when the object has no `name:` — the file stem, or the
|
|
1165
|
+
// folder for an `index` file. A kebab-case stem becomes PascalCase, the way a
|
|
1166
|
+
// component is named where it is used.
|
|
1167
|
+
function ownerStem(rel) {
|
|
1168
|
+
const parts = String(rel).split("/");
|
|
1169
|
+
let base = parts.pop().replace(/\.[^.]*$/, "");
|
|
1170
|
+
if (/^index$/i.test(base) && parts.length) base = parts.pop();
|
|
1171
|
+
if (IDENT_RE.test(base)) return base;
|
|
1172
|
+
let n = base
|
|
1173
|
+
.split(/[^A-Za-z0-9_$]+/)
|
|
1174
|
+
.filter(Boolean)
|
|
1175
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
1176
|
+
.join("");
|
|
1177
|
+
if (!n) n = "Component";
|
|
1178
|
+
return /^\d/.test(n) ? "_" + n : n;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// The end of a `const X = …` value: the `;` or the line break at depth 0 that
|
|
1182
|
+
// ends the statement. A line that ends on an operator, or a next line that
|
|
1183
|
+
// starts with one, continues it.
|
|
1184
|
+
function valueEnd(m, v) {
|
|
1185
|
+
let d = 0;
|
|
1186
|
+
for (let k = v; k < m.length; k++) {
|
|
1187
|
+
const ch = m[k];
|
|
1188
|
+
if (ch === "(" || ch === "[" || ch === "{") d++;
|
|
1189
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
1190
|
+
if (d === 0) return k - 1;
|
|
1191
|
+
d--;
|
|
1192
|
+
} else if (d === 0 && ch === ";") return k - 1;
|
|
1193
|
+
else if (d === 0 && ch === "\n") {
|
|
1194
|
+
let p = k - 1;
|
|
1195
|
+
while (p > v && (m[p] === " " || m[p] === "\t" || m[p] === "\r")) p--;
|
|
1196
|
+
let n = k + 1;
|
|
1197
|
+
while (n < m.length && /\s/.test(m[n])) n++;
|
|
1198
|
+
if (!/[=+\-*/,(?:&|]/.test(m[p]) && !/[.?:+\-*/&|]/.test(m[n] || "")) return p;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return m.length - 1;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function objectDefs(c, have, sfc) {
|
|
1205
|
+
const { src, m, starts, rel } = c;
|
|
1206
|
+
const defs = [];
|
|
1207
|
+
const defParens = new Set();
|
|
1208
|
+
const taken = new Set(have.map((d) => d.name));
|
|
1209
|
+
const trim = (b) => {
|
|
1210
|
+
let k = b - 1;
|
|
1211
|
+
while (k > 0 && /\s/.test(m[k])) k--;
|
|
1212
|
+
return k;
|
|
1213
|
+
};
|
|
1214
|
+
const add = (d) => {
|
|
1215
|
+
if (!d.name || MEMBER_KW.has(d.name)) return null;
|
|
1216
|
+
d.s = lineOf(starts, d.from);
|
|
1217
|
+
d.e = lineOf(starts, Math.max(d.from, d.to));
|
|
1218
|
+
defs.push(d);
|
|
1219
|
+
return d;
|
|
1220
|
+
};
|
|
1221
|
+
// A top-level `const NAME = …`: where a shorthand key's value is declared.
|
|
1222
|
+
const declOf = (name) => {
|
|
1223
|
+
const re = new RegExp(`^[ \\t]*(?:export[ \\t]+)?(?:const|let|var)[ \\t]+${name.replace(/\$/g, "\\$")}[ \\t]*(?::[^=\\n]+)?=[ \\t]*`, "m");
|
|
1224
|
+
const x = re.exec(m);
|
|
1225
|
+
return x ? { from: x.index, v: x.index + x[0].length } : null;
|
|
1226
|
+
};
|
|
1227
|
+
// A value that is a `require(…)` / `import(…)` is a RE-EXPORT: the name is
|
|
1228
|
+
// defined in the other file, and a constant here would make this barrel look
|
|
1229
|
+
// like its definition (`exports.x = require("./store").x`).
|
|
1230
|
+
const reexport = (v) => /^(?:await[ \t]+)?(?:require|import)[ \t]*\(/.test(m.slice(v, v + 40));
|
|
1231
|
+
const constant = (name, from, to, v) => {
|
|
1232
|
+
if (!IDENT_RE.test(name) || taken.has(name) || (v != null && reexport(v))) return;
|
|
1233
|
+
taken.add(name);
|
|
1234
|
+
add({ name, qname: name, kind: "const", from, to, exported: true });
|
|
1235
|
+
};
|
|
1236
|
+
// E2 for an object whose members are data: each key is a constant. A
|
|
1237
|
+
// function-valued member is a function, not a constant, and is left alone.
|
|
1238
|
+
const constantsOf = (open, close) => {
|
|
1239
|
+
for (const [a, b] of objectMembers(m, open, close)) {
|
|
1240
|
+
const mem = memberOf(m, a, b);
|
|
1241
|
+
if (!mem || mem.type === "fn") continue;
|
|
1242
|
+
if (mem.type === "short") {
|
|
1243
|
+
const d = declOf(mem.name);
|
|
1244
|
+
if (d && !fnValueAt(m, d.v)) constant(mem.name, d.from, valueEnd(m, d.v), d.v);
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
constant(mem.name, a, trim(b), mem.v);
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
// ── E1: the default-exported object ──
|
|
1252
|
+
let owner = null;
|
|
1253
|
+
DEFAULT_OBJECT.lastIndex = 0;
|
|
1254
|
+
const dx = DEFAULT_OBJECT.exec(m);
|
|
1255
|
+
if (dx) {
|
|
1256
|
+
const open = dx.index + dx[0].length - 1;
|
|
1257
|
+
const close = matchPairQuiet(m, open, "{", "}");
|
|
1258
|
+
const top = close > open ? objectMembers(m, open, close).map(([a, b]) => ({ a, b, mem: memberOf(m, a, b) })).filter((x) => x.mem) : [];
|
|
1259
|
+
const shaped = top.some(
|
|
1260
|
+
(x) => x.mem.type === "fn" || (OBJ_SECTIONS.has(x.mem.name) && (x.mem.type === "obj" || x.mem.type === "short")) || x.mem.name === "mixins" || x.mem.name === "extends"
|
|
1261
|
+
);
|
|
1262
|
+
if (shaped) {
|
|
1263
|
+
const nameMem = top.find((x) => x.mem.type === "val" && x.mem.name === "name");
|
|
1264
|
+
const named = nameMem ? /^\s*(['"`])([A-Za-z_$][\w$]*)\1/.exec(src.slice(nameMem.mem.v, nameMem.mem.v + 120)) : null;
|
|
1265
|
+
const oname = named ? named[2] : ownerStem(rel);
|
|
1266
|
+
const bases = [];
|
|
1267
|
+
for (const x of top) {
|
|
1268
|
+
if (x.mem.name === "mixins" && x.mem.type === "arr")
|
|
1269
|
+
for (const w of m.slice(x.mem.open + 1, x.mem.close).split(",")) if (IDENT_RE.test(w.trim())) bases.push(w.trim());
|
|
1270
|
+
if (x.mem.name === "extends" && x.mem.type === "val") {
|
|
1271
|
+
const w = m.slice(x.mem.v, x.b).trim();
|
|
1272
|
+
if (IDENT_RE.test(w)) bases.push(w);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
owner = add({ name: oname, qname: oname, kind: "class", from: dx.index, to: close, exported: true, ...(bases.length ? { bases: bases.slice(0, 8) } : {}) });
|
|
1276
|
+
const method = (name, from, to, paren) => {
|
|
1277
|
+
if (paren != null) defParens.add(paren);
|
|
1278
|
+
add({ name, qname: `${oname}.${name}`, kind: "method", from, to, exported: false });
|
|
1279
|
+
};
|
|
1280
|
+
const section = (open2, close2) => {
|
|
1281
|
+
for (const [a, b] of objectMembers(m, open2, close2)) {
|
|
1282
|
+
const mem = memberOf(m, a, b);
|
|
1283
|
+
if (!mem) continue;
|
|
1284
|
+
if (mem.type === "fn") method(mem.name, a, trim(b), mem.paren);
|
|
1285
|
+
else if (mem.type === "obj" && mem.close > mem.open) {
|
|
1286
|
+
// `label: { get() {…}, set(v) {…} }` and a watcher's `{ handler() {…} }`
|
|
1287
|
+
// are ONE member whose behaviour sits in its inner functions.
|
|
1288
|
+
const inner = objectMembers(m, mem.open, mem.close).map(([p, q]) => memberOf(m, p, q)).filter(Boolean);
|
|
1289
|
+
if (inner.some((i) => i.type === "fn" && /^(get|set|handler)$/.test(i.name))) {
|
|
1290
|
+
for (const i of inner) if (i.type === "fn" && i.paren != null) defParens.add(i.paren);
|
|
1291
|
+
method(mem.name, a, trim(b), null);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
for (const x of top) {
|
|
1297
|
+
if (x.mem.type === "fn") method(x.mem.name, x.a, trim(x.b), x.mem.paren);
|
|
1298
|
+
else if (OBJ_SECTIONS.has(x.mem.name) && x.mem.type === "obj" && x.mem.close > x.mem.open) section(x.mem.open, x.mem.close);
|
|
1299
|
+
else if (OBJ_SECTIONS.has(x.mem.name) && x.mem.type === "short") {
|
|
1300
|
+
// `const mutations = { … }; export default { mutations }` — the section
|
|
1301
|
+
// is declared above and named here.
|
|
1302
|
+
const d = declOf(x.mem.name);
|
|
1303
|
+
if (d && m[d.v] === "{") section(d.v, matchPairQuiet(m, d.v, "{", "}"));
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
} else if (close > open) constantsOf(open, close);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// ── E2: exported constants ──
|
|
1310
|
+
const EXPORT_CONST = /^[ \t]*export[ \t]+(?:const|let|var)[ \t]+([A-Za-z_$][\w$]*)[ \t]*(?::[^=\n]+)?=[ \t]*/gm;
|
|
1311
|
+
let x;
|
|
1312
|
+
// A Svelte `export let` is a PROP the parent passes in, not a constant.
|
|
1313
|
+
while (!(sfc && sfc.svelte) && (x = EXPORT_CONST.exec(m))) {
|
|
1314
|
+
const v = x.index + x[0].length;
|
|
1315
|
+
if (fnValueAt(m, v)) continue;
|
|
1316
|
+
constant(x[1], x.index, valueEnd(m, v), v);
|
|
1317
|
+
}
|
|
1318
|
+
const EXPORTS_DOT = /^[ \t]*(?:module[ \t]*\.[ \t]*)?exports[ \t]*\.[ \t]*([A-Za-z_$][\w$]*)[ \t]*=[ \t]*/gm;
|
|
1319
|
+
while ((x = EXPORTS_DOT.exec(m))) {
|
|
1320
|
+
const v = x.index + x[0].length;
|
|
1321
|
+
if (fnValueAt(m, v) || /^[A-Za-z_$][\w$]*[ \t]*[;\n]/.test(m.slice(v, v + 80))) continue;
|
|
1322
|
+
constant(x[1], x.index, valueEnd(m, v), v);
|
|
1323
|
+
}
|
|
1324
|
+
const MODULE_OBJECT = /^[ \t]*module[ \t]*\.[ \t]*exports[ \t]*=[ \t]*\{/gm;
|
|
1325
|
+
while ((x = MODULE_OBJECT.exec(m))) {
|
|
1326
|
+
const open = x.index + x[0].length - 1;
|
|
1327
|
+
const close = matchPairQuiet(m, open, "{", "}");
|
|
1328
|
+
if (close > open) constantsOf(open, close);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// ── E3: a component with no object to name ──
|
|
1332
|
+
// `<script setup>`, a Svelte instance script, or no script at all: ONE class
|
|
1333
|
+
// symbol, so the component exists in the map, the names and the hints.
|
|
1334
|
+
if (sfc && !owner && (sfc.setup || sfc.svelte || !sfc.blocks.length)) {
|
|
1335
|
+
const from = sfc.blocks.length ? sfc.blocks[0][0] : 0;
|
|
1336
|
+
const to = sfc.blocks.length ? Math.max(from, sfc.blocks[sfc.blocks.length - 1][1] - 1) : Math.max(0, src.length - 1);
|
|
1337
|
+
const name = ownerStem(rel);
|
|
1338
|
+
// NAME-ONLY: it owns no line. `<script setup>` code is module scope, and
|
|
1339
|
+
// an alias declared there (`const store = new OrderStore()`) must stay
|
|
1340
|
+
// visible to every function below it, exactly as it was before W5b.
|
|
1341
|
+
if (!taken.has(name)) add({ name, qname: name, kind: "class", from, to, exported: true, nameOnly: true });
|
|
1342
|
+
}
|
|
1343
|
+
return { defs, defParens };
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1064
1346
|
// ── declarations: Python (indentation) ──────────────────────────────────────
|
|
1065
1347
|
function pyDefs(c) {
|
|
1066
1348
|
const { src, m, starts, strAt } = c;
|
|
@@ -2015,7 +2297,13 @@ function finalize(c, lang, defs, calls, imports, exportsSet, extractor) {
|
|
|
2015
2297
|
|
|
2016
2298
|
const owner = new Int32Array(nLines + 2).fill(-1);
|
|
2017
2299
|
const order = defs.map((_, i) => i).sort((a, b) => defs[b].e - defs[b].s - (defs[a].e - defs[a].s));
|
|
2018
|
-
|
|
2300
|
+
// W5b (E2): a `const` is a NAME, not a body. A call inside its value (`axios.create(…)`)
|
|
2301
|
+
// stays with whatever holds the constant — the module, most often — so a
|
|
2302
|
+
// constant symbol always carries `calls: []`.
|
|
2303
|
+
for (const i of order) {
|
|
2304
|
+
if (defs[i].kind === "const" || defs[i].nameOnly) continue;
|
|
2305
|
+
for (let L = defs[i].s; L <= defs[i].e && L <= nLines; L++) owner[L] = i;
|
|
2306
|
+
}
|
|
2019
2307
|
|
|
2020
2308
|
// W2 (G2): instance aliases, scoped. `classAt(line)` is the innermost class
|
|
2021
2309
|
// holding a line; `ownerOf(line)` the innermost symbol (−1 = the module).
|
|
@@ -2258,6 +2546,7 @@ function context(item) {
|
|
|
2258
2546
|
function sfcSource(src) {
|
|
2259
2547
|
const out = src.split("");
|
|
2260
2548
|
let lang = "js";
|
|
2549
|
+
let setup = false;
|
|
2261
2550
|
const keep = [];
|
|
2262
2551
|
const lower = src.toLowerCase();
|
|
2263
2552
|
const RE = /<script\b([^>]*)>/gi;
|
|
@@ -2267,6 +2556,9 @@ function sfcSource(src) {
|
|
|
2267
2556
|
const close = lower.indexOf("</script>", open);
|
|
2268
2557
|
const end = close < 0 ? src.length : close;
|
|
2269
2558
|
if (/lang[ \t]*=[ \t]*["']?(ts|typescript)/i.test(x[1])) lang = "ts";
|
|
2559
|
+
// W5b (E3): `<script setup>` has no object to name, so the component is
|
|
2560
|
+
// named from its file instead.
|
|
2561
|
+
if (/(^|\s)setup(\s|=|$)/i.test(x[1])) setup = true;
|
|
2270
2562
|
keep.push([open, end]);
|
|
2271
2563
|
RE.lastIndex = end;
|
|
2272
2564
|
}
|
|
@@ -2279,14 +2571,16 @@ function sfcSource(src) {
|
|
|
2279
2571
|
at = b;
|
|
2280
2572
|
}
|
|
2281
2573
|
blank(at, src.length);
|
|
2282
|
-
return { src: out.join(""), lang };
|
|
2574
|
+
return { src: out.join(""), lang, blocks: keep, setup };
|
|
2283
2575
|
}
|
|
2284
2576
|
|
|
2285
2577
|
function extractOne(item, borrowed) {
|
|
2286
2578
|
let { lang, src } = item;
|
|
2287
2579
|
resetCoverage();
|
|
2580
|
+
let sfc = null;
|
|
2288
2581
|
if (lang === "vue" || lang === "svelte") {
|
|
2289
2582
|
const s = sfcSource(src);
|
|
2583
|
+
sfc = { blocks: s.blocks, setup: s.setup, svelte: lang === "svelte" };
|
|
2290
2584
|
src = s.src;
|
|
2291
2585
|
lang = s.lang;
|
|
2292
2586
|
item = { ...item, src, lang };
|
|
@@ -2312,6 +2606,10 @@ function extractOne(item, borrowed) {
|
|
|
2312
2606
|
const { defs: rdefs, regParens, exportsSet: hexports } = braceDefs(c, lang);
|
|
2313
2607
|
resetCoverage();
|
|
2314
2608
|
for (const d of rdefs) if (d.kind === "route") defs.push({ ...d, s: lineOf(c.starts, d.from), e: lineOf(c.starts, Math.max(d.from, d.to)) });
|
|
2609
|
+
// W5b: the walker records a method only inside a class. An object's
|
|
2610
|
+
// members, the constants and an SFC's component come from the same pass
|
|
2611
|
+
// the heuristic rung runs, so both rungs name the same symbols.
|
|
2612
|
+
if (lang === "js" || lang === "ts") defs.push(...objectDefs(c, defs, sfc).defs);
|
|
2315
2613
|
for (const p of regParens) reg.add(lineOf(c.starts, p));
|
|
2316
2614
|
for (const cc of calls) if (reg.has(cc.line) && ARGS_WORTH.test(cc.name)) cc.reg = true;
|
|
2317
2615
|
defs.sort((a, b) => a.from - b.from);
|
|
@@ -2338,6 +2636,13 @@ function extractOne(item, borrowed) {
|
|
|
2338
2636
|
return finalize(c, lang, defs, scanCalls(c, defParens, regParens), importsFor(lang, c.src, c.m, c.starts), null, HEURISTIC);
|
|
2339
2637
|
}
|
|
2340
2638
|
const { defs, defParens, regParens, exportsSet } = braceDefs(c, lang);
|
|
2639
|
+
if (lang === "js" || lang === "ts") {
|
|
2640
|
+
const o = objectDefs(c, defs, sfc);
|
|
2641
|
+
defs.push(...o.defs);
|
|
2642
|
+
// APPENDED, never sorted in: a file with no object keeps every symbol in
|
|
2643
|
+
// the order 1.9.0 wrote it, so its record is the same bytes.
|
|
2644
|
+
for (const p of o.defParens) defParens.add(p);
|
|
2645
|
+
}
|
|
2341
2646
|
braceDecorators(c, lang, defs);
|
|
2342
2647
|
return finalize(c, lang, defs, scanCalls(c, defParens, regParens), importsFor(lang, c.src, c.m, c.starts), exportsSet, HEURISTIC);
|
|
2343
2648
|
}
|