@aroman22/codegraph-vba 1.15.0 → 1.16.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.
Files changed (49) hide show
  1. package/dist/bin/daemon-release.d.ts +7 -0
  2. package/dist/db/queries.d.ts +30 -0
  3. package/dist/extraction/access-erd-extractor.d.ts +57 -0
  4. package/dist/extraction/extraction-version.d.ts +1 -1
  5. package/dist/extraction/grammars.d.ts +20 -0
  6. package/dist/extraction/index.d.ts +1 -0
  7. package/dist/extraction/parse-pool.d.ts +8 -3
  8. package/dist/extraction/sql-query-extractor.d.ts +16 -14
  9. package/dist/extraction/sql-table-scan.d.ts +184 -0
  10. package/dist/extraction/tree-sitter.d.ts +10 -1
  11. package/dist/extraction/vba/call-sweep.d.ts +2 -7
  12. package/dist/extraction/vba/calls.d.ts +24 -4
  13. package/dist/extraction/vba/constants.d.ts +7 -14
  14. package/dist/extraction/vba/context.d.ts +443 -7
  15. package/dist/extraction/vba/controls.d.ts +18 -1
  16. package/dist/extraction/vba/declarations.d.ts +1 -6
  17. package/dist/extraction/vba/dims.d.ts +11 -7
  18. package/dist/extraction/vba/docmd.d.ts +29 -2
  19. package/dist/extraction/vba/enums-consts.d.ts +7 -14
  20. package/dist/extraction/vba/error-channel.d.ts +57 -0
  21. package/dist/extraction/vba/errors.d.ts +64 -0
  22. package/dist/extraction/vba/filesystem-statements.d.ts +23 -0
  23. package/dist/extraction/vba/implements.d.ts +1 -6
  24. package/dist/extraction/vba/labels.d.ts +26 -0
  25. package/dist/extraction/vba/module-vars.d.ts +36 -0
  26. package/dist/extraction/vba/options.d.ts +88 -0
  27. package/dist/extraction/vba/parameters.d.ts +35 -0
  28. package/dist/extraction/vba/procedures.d.ts +1 -9
  29. package/dist/extraction/vba/rules.d.ts +10 -5
  30. package/dist/extraction/vba/runtime-objects.d.ts +59 -0
  31. package/dist/extraction/vba/signature.d.ts +57 -0
  32. package/dist/extraction/vba/sql-wrapper.d.ts +76 -3
  33. package/dist/extraction/vba/text-utils.d.ts +62 -2
  34. package/dist/extraction/vba-extractor.d.ts +18 -1
  35. package/dist/extraction/vba-form-extractor.d.ts +28 -14
  36. package/dist/extraction/vba-preprocess.d.ts +89 -3
  37. package/dist/extraction/vba-source.d.ts +0 -10
  38. package/dist/extraction/vba-test-manifest-extractor.d.ts +0 -6
  39. package/dist/mcp/daemon-paths.d.ts +6 -0
  40. package/dist/mcp/daemon-registry.d.ts +82 -7
  41. package/dist/mcp/daemon-watchdog.d.ts +12 -0
  42. package/dist/mcp/daemon.d.ts +20 -1
  43. package/dist/mcp/proxy.d.ts +32 -0
  44. package/dist/project-config.d.ts +43 -2
  45. package/dist/resolution/index.d.ts +47 -2
  46. package/dist/resolution/name-matcher.d.ts +25 -0
  47. package/dist/resolution/vba-runtime-objects.d.ts +11 -11
  48. package/dist/types.d.ts +13 -5
  49. package/package.json +7 -7
@@ -1,7 +1,68 @@
1
- import { VbaExtractorContext } from './context';
2
- export declare function scanSqlInLine(ctx: VbaExtractorContext, line: string, lineNum: number, dedupe: Set<string>, sqlVariables: Map<string, string>): void;
1
+ import { VbaExtractorContext, ProcInfo } from './context';
3
2
  /**
4
- * #13 fix: `sql = sql & "..."` (self-referential concatenation) must
3
+ * Issue #252 `metadata.synthesizedBy` for a table reached through SQL
4
+ * assigned to a binding property at RUNTIME. Deliberately distinct from the
5
+ * `.form.txt` sweep's static `vba-row-source` so an audit can tell the two
6
+ * provenances apart: one is what the designer stored, the other is what the
7
+ * code actually binds.
8
+ */
9
+ export declare const RUNTIME_BINDING_SYNTHESIZER = "vba-row-source-dynamic";
10
+ /**
11
+ * Issue #244 — the wrapper list used when `codegraph.json` →
12
+ * `vba.sqlWrappers` is absent. A STRICT SUPERSET of the pre-#244 behaviour:
13
+ *
14
+ * - `db` covers every receiver the old `…db\b` regex reached (see
15
+ * {@link receiverMatches} for the suffix arm that preserves it) plus the
16
+ * `db`-prefixed locals it missed (`dbUse`, `dbToUse`, `m_dbLanzadera`).
17
+ * - `getdb` covers the per-backend accessor family (`getdbHPS()`,
18
+ * `getdbExpedientes()`, `getdbLanzadera()`) the issue measured at 26-36%
19
+ * of all execution sites.
20
+ * - `CurrentDb` / `DBEngine` are the Access built-ins.
21
+ * - `qd` / `qdf` are the conventional DAO `QueryDef` receivers.
22
+ * - `DoCmd.RunSQL` is the Access statement form (both the literal and the
23
+ * variable spelling, previously two dedicated regexes).
24
+ * - `Connection.Execute` / `Recordset.Open` are the ADO pair.
25
+ *
26
+ * Project-specific accessors that do not fit these names are added through
27
+ * `vba.sqlWrappers`, which EXTENDS this list rather than replacing it.
28
+ */
29
+ export declare const DEFAULT_SQL_WRAPPERS: readonly string[];
30
+ /** One parsed wrapper entry. Both fields are lowercase — VBA is case-insensitive. */
31
+ interface SqlWrapperMatcher {
32
+ /** Receiver name fragment, e.g. `getdb` from `"getdb"` or `cnn` from `"cnn.Execute"`. */
33
+ receiver: string;
34
+ /** Method name, or `null` for a bare entry (which implies {@link DEFAULT_WRAPPER_METHODS}). */
35
+ method: string | null;
36
+ }
37
+ /**
38
+ * The per-extractor compiled wrapper state. Built ONCE per `VbaExtractor`
39
+ * (see `vba-extractor.ts`) and cached on `VbaExtractorContext` — the two
40
+ * RegExps are stateful (`/g`) and `scanSqlInLine` runs on every line of every
41
+ * file, so re-compiling them per line (which is what the pre-#244 code did)
42
+ * is pure waste on the hottest path in VBA extraction.
43
+ */
44
+ export interface CompiledSqlWrappers {
45
+ /** Literal form — `<receiver>[()].<method> "…"`. Stateful; reset before use. */
46
+ readonly literalRe: RegExp;
47
+ /** Variable form — `<receiver>[()].<method> <identifier>`. Stateful; reset before use. */
48
+ readonly varRe: RegExp;
49
+ /** Parsed entries, defaults first, project entries appended. */
50
+ readonly matchers: readonly SqlWrapperMatcher[];
51
+ }
52
+ /**
53
+ * Parse the plain-string wrapper entries into matchers and pair them with a
54
+ * fresh pair of scanning RegExps.
55
+ *
56
+ * `configured` entries are APPENDED to {@link DEFAULT_SQL_WRAPPERS}, never
57
+ * substituted for them: a project that names its own accessor must not lose
58
+ * `CurrentDb` in the trade. Entries are accepted in exactly two forms — a
59
+ * bare identifier fragment (`"getdb"`) or a `receiver.method` pair
60
+ * (`"cnn.Execute"`). Anything else is dropped here; `project-config.ts`
61
+ * already warned about it at load time.
62
+ */
63
+ export declare function compileSqlWrappers(configured?: readonly string[]): CompiledSqlWrappers;
64
+ /**
65
+ * Issue #13: `sql = sql & "..."` (self-referential concatenation) must
5
66
  * ACCUMULATE the new fragment onto whatever was already tracked for
6
67
  * `varName`, not overwrite it. Overwriting silently dropped earlier
7
68
  * fragments' tables — typically the initial `FROM <table>` in
@@ -14,4 +75,16 @@ export declare function scanSqlInLine(ctx: VbaExtractorContext, line: string, li
14
75
  * behavior is unchanged.
15
76
  */
16
77
  export declare function trackSqlVariableAssignment(lines: string[], lineIndex: number, sqlVariables: Map<string, string>): void;
78
+ /**
79
+ * Issue #255: scan one line for domain-aggregate calls and emit the saved-query
80
+ * reference their second argument names.
81
+ *
82
+ * Deliberately NOT handled: a domain spelled as a full SQL statement
83
+ * (`DLookup("N", "SELECT Id FROM T")`), which Access also accepts. It is
84
+ * rejected by `looksLikeSavedQueryName`'s verb gate and produces nothing —
85
+ * the same silence T11 chose for every literal it cannot classify.
86
+ */
87
+ export declare function scanDomainFunctionsInLine(ctx: VbaExtractorContext, line: string, lineNum: number, dedupe: Set<string>, caller: ProcInfo): void;
88
+ export declare function scanSqlInLine(ctx: VbaExtractorContext, line: string, lineNum: number, dedupe: Set<string>, sqlVariables: Map<string, string>, caller?: ProcInfo): void;
89
+ export {};
17
90
  //# sourceMappingURL=sql-wrapper.d.ts.map
@@ -11,8 +11,17 @@ export declare function foldVisibility(raw: string): 'public' | 'private';
11
11
  * call-site patterns inside `"..."` spans are invisible to CALL_RE and
12
12
  * the statement-form detectors. Column positions are preserved (each
13
13
  * character is replaced 1-for-1) so any col-based metadata stays correct.
14
+ *
15
+ * Issue #265: `filler` selects the replacement character. The default `' '`
16
+ * is what every column-sensitive scanner wants — a masked literal reads as
17
+ * blank space. The statement-call detectors need the opposite: they must
18
+ * still be able to tell `MsgBox "x"` (an argument list, therefore
19
+ * unambiguously a call) from a bare `MsgBox` read, and a space-masked line
20
+ * collapses the two. Passing `'_'` keeps the literal *visible* as an opaque
21
+ * token while preserving both the 1-for-1 column mapping and the property
22
+ * that nothing inside the literal can be parsed as VBA syntax.
14
23
  */
15
- export declare function maskStringContent(line: string): string;
24
+ export declare function maskStringContent(line: string, filler?: string): string;
16
25
  /**
17
26
  * #13 helper: escape a variable name for safe interpolation into the
18
27
  * self-reference RegExp built by `trackSqlVariableAssignment`. VBA
@@ -26,7 +35,6 @@ export declare function parseConstDeclarations(body: string): Array<{
26
35
  asType: string;
27
36
  value: string | null;
28
37
  }>;
29
- export declare function splitOutsideVbaStrings(value: string, separator: string): string[];
30
38
  /**
31
39
  * Issue #50 helper: return true iff `line.charAt(fromIndex..)` (after the
32
40
  * matched TempVars site, including any trailing whitespace) holds an `=`
@@ -65,4 +73,56 @@ export declare function parseEventHandlerName(name: string): {
65
73
  controlName: string;
66
74
  eventName: string;
67
75
  } | null;
76
+ /**
77
+ * Issue #247 helper: parse a FORM-LEVEL Access event-handler Sub name.
78
+ *
79
+ * `parseEventHandlerName` above deliberately refuses `Form_*` because a
80
+ * form-level event fires on the form object, not on a control — routing it
81
+ * through the control path would synthesize a bogus `form-instance-control`
82
+ * node literally named `Form`. This helper is the other half of that
83
+ * decision: it recognises exactly the names the control parser rejects and
84
+ * hands them to the caller so they can be wired to the sibling
85
+ * `form-layout` / `report-layout` node instead.
86
+ *
87
+ * The owner segment must be exactly `Form` or `Report` (case-insensitive,
88
+ * matching VBA's case-insensitive identifiers), and the suffix must be a
89
+ * known Access event name. That second gate is what separates the real
90
+ * lifecycle handler `Form_Load` from an ordinary class method that merely
91
+ * happens to be called `Form_Helper`.
92
+ */
93
+ export declare function parseFormLevelEventHandlerName(name: string): {
94
+ ownerName: string;
95
+ eventName: string;
96
+ } | null;
97
+ /**
98
+ * Sibling layout extension implied by an Access code-behind prefix.
99
+ * `Form_*` binds to a `.form.txt`; `Report_*` binds to a `.report.txt`.
100
+ */
101
+ export type CodeBehindExt = '.form.txt' | '.report.txt';
102
+ /**
103
+ * Issue #249 helper: the `Form_` / `Report_` code-behind prefix carried by a
104
+ * module's RESOLVED `Attribute VB_Name`, used as the fallback when the file
105
+ * on disk does not carry the prefix in its basename.
106
+ *
107
+ * Both binding sites (the event-handler synthesis in `procedures.ts` and the
108
+ * `Me.<Control>` sweep in `controls.ts`) keep their own basename check as the
109
+ * fast path and consult this only on a basename miss, so the common case —
110
+ * a Dysflow export whose filename and `VB_Name` agree — is byte-for-byte
111
+ * unchanged.
112
+ *
113
+ * `classNamePrefix` is `null` for `.bas` modules and holds the resolved
114
+ * `VB_Name` (or the extension-stripped basename when the module carries no
115
+ * `VB_Name`) for `.cls` modules, so this fallback can only ever fire for a
116
+ * class module. That is deliberate: the guard that keeps a plain service
117
+ * class such as `InformeRiesgoPDFServicio.cls` — whose methods
118
+ * (`GenerarHTML_Principal`, `GetEstilosCSS_PDF`) look like event handlers to
119
+ * a naive `<X>_<Y>` split — from synthesizing hundreds of spurious
120
+ * `form-instance-control` stubs is that NEITHER its filename NOR its
121
+ * `VB_Name` starts with `Form_` / `Report_`. Widening the prefix test would
122
+ * reopen exactly that hole, so the test stays the same canonical Access
123
+ * naming convention; only the string it is applied to widens.
124
+ *
125
+ * The trailing `.+` is load-bearing: a bare `Form_` names no form.
126
+ */
127
+ export declare function codeBehindExtFromVbName(classNamePrefix: string | null): CodeBehindExt | null;
68
128
  //# sourceMappingURL=text-utils.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { ExtractionResult } from '../types';
2
+ import type { VbaExtractionOptions } from './vba/options';
2
3
  import type { VbaExtractionRule } from './vba/rules';
3
4
  /**
4
5
  * Issue #152: per-file fanout cap for `RaiseEvent <EventName>` edges. An
@@ -62,7 +63,6 @@ export declare class VbaExtractor {
62
63
  private filePath;
63
64
  private source;
64
65
  private ctx;
65
- private vbaTargets?;
66
66
  /**
67
67
  * Issue #152: per-file fanout cap for `RaiseEvent <EventName>` edges.
68
68
  * When a single event is raised more than this many times in one file,
@@ -72,6 +72,23 @@ export declare class VbaExtractor {
72
72
  * the orchestrator: callers that don't pass a value get the default.
73
73
  */
74
74
  private maxRaiseFanout;
75
+ /**
76
+ * Issue #243: the full options object, retained so classifiers that grow
77
+ * a new knob read it from one place instead of a new private field per
78
+ * knob. `targets`/`maxRaiseFanout` keep their dedicated fields because
79
+ * they are read on hot paths.
80
+ */
81
+ private options;
82
+ /**
83
+ * Issue #243 — the options-object form. Every in-repo call site uses this.
84
+ */
85
+ constructor(filePath: string, source: string, options?: VbaExtractionOptions);
86
+ /**
87
+ * @deprecated Pass a {@link VbaExtractionOptions} object instead:
88
+ * `new VbaExtractor(filePath, source, { targets, maxRaiseFanout })`.
89
+ * The positional 3rd/4th parameters are kept working for one release so
90
+ * out-of-repo callers are not broken by #243; they will be removed after.
91
+ */
75
92
  constructor(filePath: string, source: string, vbaTargets?: Record<string, boolean>, maxRaiseFanout?: number);
76
93
  extract(): ExtractionResult;
77
94
  /**
@@ -17,6 +17,14 @@ export declare class VbaFormExtractor {
17
17
  * stays self-contained per the project's per-extractor state rule).
18
18
  */
19
19
  private synthClassNodeIds;
20
+ /**
21
+ * Issue #256: external-backend (`IN "<path>"`) node ids already emitted
22
+ * for this form. Same per-extractor de-dup discipline as
23
+ * `synthClassNodeIds`, keyed on the node id — which is derived from the
24
+ * NORMALIZED backend path, so two bindings naming the same `.accdb`
25
+ * share one node while each still emits its own edge.
26
+ */
27
+ private externalBackendNodeIds;
20
28
  constructor(filePath: string, source: string);
21
29
  extract(): ExtractionResult;
22
30
  private createFileNode;
@@ -63,23 +71,11 @@ export declare class VbaFormExtractor {
63
71
  private emitExpressionHandlers;
64
72
  private emitExpressionHandler;
65
73
  private emitSourceObjectReference;
66
- /**
67
- * Issue #49 — copy of `VbaExtractor.SQL_TABLE_RE`. Same source / flags:
68
- * captures the table name that follows `FROM`/`JOIN`/`INTO`/`UPDATE`,
69
- * tolerates bracketed `[Order Details]` identifiers and `\p{L}` Unicode
70
- * identifiers, and allows an optional schema prefix (`[dbo].[tblA]`).
71
- *
72
- * We duplicate the regex (rather than exporting it from `VbaExtractor`)
73
- * because `VbaExtractor.SQL_TABLE_RE` is `private static` and the
74
- * project's per-extractor state rule keeps each extractor's helpers
75
- * self-contained — see the file-level JSDoc on `VbaExtractor`.
76
- */
77
- private static readonly SQL_TABLE_RE;
78
74
  /**
79
75
  * Issue #49 — classify a RecordSource/RowSource value as SQL.
80
76
  * Anything starting with a SQL keyword (`SELECT`, `PARAMETERS`, `WITH`,
81
77
  * `UPDATE`, `INSERT`, `DELETE`) — case-insensitive — is treated as a
82
- * SQL statement and run through `SQL_TABLE_RE`. Anything else is a
78
+ * SQL statement and run through `scanSqlTables`. Anything else is a
83
79
  * bare table-or-query name and emitted as a single reference.
84
80
  *
85
81
  * `WITH` is included because Access/JET supports CTE-style `WITH` queries
@@ -103,6 +99,20 @@ export declare class VbaFormExtractor {
103
99
  * name — same dual-match `vba-sql-impact`'s `extractFormBindings` does).
104
100
  */
105
101
  private emitTableReference;
102
+ /**
103
+ * Issue #256 — emit one `references` edge from `sourceNodeId` to the
104
+ * external database file an Access `IN "<path>"` clause points at.
105
+ *
106
+ * The target node is built by the shared `buildExternalBackendNode` so
107
+ * it is byte-identical to the node the in-code SQL sweep and the saved-
108
+ * query extractor emit for the same path: the graph ends up with ONE
109
+ * node per external backend, however many places name it. The edge
110
+ * carries only `synthesizedBy` — the `external` / `backendPath` facts
111
+ * describe the node, and live there.
112
+ *
113
+ * `backendPath` is already normalized by `scanSqlExternalBackends`.
114
+ */
115
+ private emitExternalBackendReference;
106
116
  /**
107
117
  * Link a bound control to its enclosing form/report's single bare table.
108
118
  * Expressions and SQL/absent RecordSource values stay metadata-only: column
@@ -112,7 +122,7 @@ export declare class VbaFormExtractor {
112
122
  private emitControlSourceReference;
113
123
  /**
114
124
  * Issue #49 — dispatch the value of a RecordSource/RowSource binding.
115
- * If SQL, run `SQL_TABLE_RE` over the value and emit one edge per
125
+ * If SQL, run `scanSqlTables` over the value and emit one edge per
116
126
  * distinct table (within-value dedup so `FROM tblA JOIN tblA` emits a
117
127
  * single edge). If a bare name, emit a single edge with the name as-is
118
128
  * — the resolver handles the dual-match against `query` and `class`
@@ -122,6 +132,10 @@ export declare class VbaFormExtractor {
122
132
  * the regex capture already includes them as part of the value, so a
123
133
  * single `.replace(/""/g, '"')` collapses them — same technique the
124
134
  * `extractStringLiterals` helper uses for its emitted `text` field.
135
+ *
136
+ * Issue #203: `scanSqlTables` also drops SQL reserved-word captures
137
+ * (`WHERE`, `ORDER`, `SET`, …) so a malformed SQL string never
138
+ * poisons the graph with phantom `class` nodes.
125
139
  */
126
140
  private emitBinding;
127
141
  }
@@ -32,6 +32,7 @@
32
32
  *
33
33
  * Source unchanged guarantee: every helper treats `src` as read-only.
34
34
  */
35
+ import type { ExtractionError } from '../types';
35
36
  /**
36
37
  * Collapse VBA line continuations: any line whose last non-newline character is
37
38
  * `_` (preceded by exactly one space, per VBA convention) is joined to the
@@ -93,8 +94,36 @@ export declare function stripVbaComments(src: string): string;
93
94
  * promote this evaluator to full integer arithmetic.
94
95
  *
95
96
  * Directives and inactive branch lines are replaced with empty strings so
96
- * downstream extraction keeps source-line parity. Unsupported/unsafe
97
- * expressions evaluate to false rather than throwing.
97
+ * downstream extraction keeps source-line parity.
98
+ *
99
+ * **Issue #206 contract change.** Previously the docstring read
100
+ * *"Unsupported/unsafe expressions evaluate to false rather than
101
+ * throwing"* — accurate, but it framed silent whole-branch source
102
+ * deletion as a safety property. It was not: false-negative extraction is
103
+ * indistinguishable from correct extraction to every downstream consumer
104
+ * (an `#If MODE = "DEBUG" Then` whose `"` throws silently blanks the
105
+ * branch and the symbol vanishes from the graph).
106
+ *
107
+ * The new contract:
108
+ *
109
+ * 1. **Unterminated `#If`/`#ElseIf`**: if the directive stack is
110
+ * non-empty after the loop ends, an `ExtractionError` (severity
111
+ * `warning`, code `unterminated_if`) is pushed onto the optional
112
+ * `errors` array AND the un-blanked lines from the earliest
113
+ * unclosed `#If` are re-emitted. An unterminated `#If` is far
114
+ * more likely a typo than a genuinely dead file tail.
115
+ * 2. **Could-not-evaluate expressions**: `evaluateConditionalExpression`
116
+ * now distinguishes `evaluated: false` (a definitive false — e.g.
117
+ * `#If 0 Then`, unknown identifier falling through to 0) from
118
+ * `could-not-evaluate` (the lexer/parser hit an unsupported token,
119
+ * or `tokenize`/`parse` threw). On could-not-evaluate, the branch
120
+ * is **kept active** (conservative for a code-intelligence tool —
121
+ * a spurious symbol is recoverable, a missing one is invisible)
122
+ * and a warning is emitted through the same `errors` channel.
123
+ *
124
+ * The `errors` channel is OPTIONAL — every existing call site (and
125
+ * existing test) continues to work without it. The orchestrator can
126
+ * supply it to surface the warnings through `ctx.errors`.
98
127
  */
99
128
  export declare function preprocessConditionalCompilation(src: string, customTargets?: Record<string, boolean>,
100
129
  /**
@@ -106,7 +135,64 @@ export declare function preprocessConditionalCompilation(src: string, customTarg
106
135
  * inner sub-totals. Undefined → no-op, zero cost (just the
107
136
  * `if (timings)` null-check on every CC line).
108
137
  */
109
- timings?: Map<string, number> | null): string;
138
+ timings?: Map<string, number> | null,
139
+ /**
140
+ * Issue #206: optional `ExtractionError` sink for diagnostics the
141
+ * preprocessor wants to surface to extraction callers:
142
+ * - `unterminated_if` (warning) — `#If` / `#ElseIf` pushed but
143
+ * never closed by `#End If` (a likely typo).
144
+ * - `unparseable_expression` (warning) — a `#If` / `#ElseIf`
145
+ * expression the lexer/parser could not handle (the branch is
146
+ * still kept active under the conservative contract).
147
+ *
148
+ * Undefined → no-op, zero cost (the `if (errors)` null-check on
149
+ * every warning site). The orchestrator passes `this.ctx.errors` so
150
+ * the warnings route through `ExtractionResult.errors`.
151
+ */
152
+ errors?: ExtractionError[],
153
+ /**
154
+ * Issue #206: optional file path included on every pushed error
155
+ * when the caller wants telemetry to know which file the warning
156
+ * came from. Independent of `errors` so a caller can attach the
157
+ * path even when collecting warnings elsewhere.
158
+ */
159
+ filePath?: string): string;
160
+ /**
161
+ * Issue #206 discriminated return value for `evaluateConditionalExpression`.
162
+ *
163
+ * - `{ kind: 'evaluated', value }` — the lexer+parser produced a
164
+ * definitive answer. `value` is the truthiness of the expression
165
+ * under VBA semantics (non-zero = true).
166
+ * - `{ kind: 'could-not-evaluate' }` — the lexer/parser threw
167
+ * (unsupported token: string literal, floating-point literal,
168
+ * unrecognized character, mismatched paren, etc.). The caller MUST
169
+ * treat this as ACTIVE under the conservative contract from
170
+ * Issue #206 — see `preprocessConditionalCompilation`.
171
+ */
172
+ export type ConditionalEvaluation = {
173
+ readonly kind: 'evaluated';
174
+ readonly value: boolean;
175
+ } | {
176
+ readonly kind: 'could-not-evaluate';
177
+ };
178
+ /**
179
+ * Issue #206: marker error class for explicitly-unsupported VBA
180
+ * conditional-compilation tokens. The legacy `throw new Error(...)`
181
+ * from `tokenize` was indistinguishable from a real bug
182
+ * (`Unexpected trailing token`, mismatched paren); both bubbled into
183
+ * the same `catch { return false }`. The new helper carries the
184
+ * specific reason so the caller can attribute warnings precisely and
185
+ * distinguish "the lexer simply cannot handle this token" from a
186
+ * legitimate parse failure (which is also routed to
187
+ * could-not-evaluate, but with the original message preserved).
188
+ */
189
+ export declare class UnparseableExpressionError extends Error {
190
+ /** Coarse category: `string_literal`, `float_literal`, `unsupported_character`. */
191
+ readonly reason: string;
192
+ /** The verbatim fragment that triggered the throw (for diagnostics). */
193
+ readonly fragment: string;
194
+ constructor(reason: string, fragment: string);
195
+ }
110
196
  export interface StringLiteralSpan {
111
197
  /** The literal content (no surrounding quotes; `""` collapsed to `"`). */
112
198
  text: string;
@@ -45,14 +45,4 @@ export interface ReadVbaSourceResult {
45
45
  * @param opts optional dependencies; see {@link ReadVbaSourceOptions}.
46
46
  */
47
47
  export declare function readVbaSource(filePath: string, opts?: ReadVbaSourceOptions): ReadVbaSourceResult;
48
- /**
49
- * String-level BOM strip — survives when the upstream `fsp.readFile(path, 'utf-8')`
50
- * already decoded the bytes (it's the only way the `\uFEFF` char survives in
51
- * a string). Used as a defensive last resort when a read site has NOT gone
52
- * through `readVbaSource` and the source text starts with the BOM marker.
53
- *
54
- * Most call sites should prefer `readVbaSource` for fresh reads; this is
55
- * a string post-process for callers that already have a `string` in hand.
56
- */
57
- export declare function stripUtf8Bom(text: string): string;
58
48
  //# sourceMappingURL=vba-source.d.ts.map
@@ -1,10 +1,4 @@
1
1
  import { ExtractionResult } from '../types';
2
- /**
3
- * Content-shape gate: is `parsed` a VBA test manifest — a top-level `tests`
4
- * array with at least one item carrying a string `procedure`? Pure; the file's
5
- * basename is gated separately by `isVbaTestManifestFile` in `grammars.ts`.
6
- */
7
- export declare function isVbaTestManifestShape(parsed: unknown): boolean;
8
2
  export declare class VbaTestManifestExtractor {
9
3
  private filePath;
10
4
  private source;
@@ -52,6 +52,12 @@ export declare function getDaemonSocketCandidates(projectRoot: string): string[]
52
52
  export declare function getDaemonSocketPath(projectRoot: string): string;
53
53
  /** Absolute path to the daemon pid lockfile for `projectRoot`. */
54
54
  export declare function getDaemonPidPath(projectRoot: string): string;
55
+ /** Root-scoped lease held while intentional release owns lifecycle cleanup. */
56
+ export declare function getDaemonReleaseLeasePath(projectRoot: string): string;
57
+ /** Exclusive marker for release-lease recovery/heartbeat publication. */
58
+ export declare function getDaemonReleaseRecoveryPath(projectRoot: string): string;
59
+ /** Short root-scoped arbitration lock shared by startup and release publication. */
60
+ export declare function getDaemonLifecyclePath(projectRoot: string): string;
55
61
  /** Structured contents of the pid lockfile. */
56
62
  export interface DaemonLockInfo {
57
63
  pid: number;
@@ -1,3 +1,24 @@
1
+ /**
2
+ * Global daemon registry + stop/list control — the discovery layer behind
3
+ * `codegraph list` and `codegraph stop [--all]`.
4
+ *
5
+ * Every per-project daemon already writes an authoritative lockfile at
6
+ * `<root>/.codegraph/daemon.pid`. That's enough to stop ONE daemon you can name,
7
+ * but there's no central place to find them ALL — which `list` and `stop --all`
8
+ * need. So each daemon also drops a tiny record under `~/.codegraph/daemons/` on
9
+ * start and removes it on graceful shutdown.
10
+ *
11
+ * The registry is a DISCOVERY index, never a source of truth: the live pid is.
12
+ * A SIGKILL'd daemon can't remove its own record, so readers prune any record
13
+ * whose pid is dead (`isProcessAlive`). Every write/read is best-effort — a
14
+ * registry hiccup must never break the daemon or a command; worst case `list`
15
+ * momentarily misses or over-lists one, which the next liveness prune corrects.
16
+ *
17
+ * Cross-platform by construction: only files + `process.kill(pid, signal)`,
18
+ * which behave consistently on macOS/Linux (real signals) and Windows (mapped to
19
+ * TerminateProcess). Validated live on all three.
20
+ */
21
+ import * as fs from 'fs';
1
22
  export interface DaemonRecord {
2
23
  /** Realpath'd project root the daemon serves. */
3
24
  root: string;
@@ -32,16 +53,70 @@ export declare function listDaemons(opts?: {
32
53
  export interface StopResult {
33
54
  root: string;
34
55
  pid: number | null;
35
- /** 'term' graceful, 'kill' force, 'not-running' stale lock, 'no-daemon' none found. */
36
- outcome: 'term' | 'kill' | 'not-running' | 'no-daemon';
56
+ outcome: 'released' | 'not-running' | 'no-daemon' | 'identity-mismatch' | 'unreachable' | 'termination-failed';
57
+ failure?: string;
58
+ }
59
+ export interface ExpectedDaemon {
60
+ root: string;
61
+ pid: number;
62
+ version: string;
63
+ socketPath: string;
64
+ startedAt: number;
65
+ }
66
+ export interface DaemonReleaseDeps {
67
+ isAlive?: (pid: number) => boolean;
68
+ waitForDeath?: (pid: number, timeoutMs: number) => Promise<boolean>;
69
+ requestControl?: (expected: ExpectedDaemon) => Promise<'releasing' | 'identity-mismatch' | 'unreachable'>;
70
+ beforeArtifactDelete?: () => void;
71
+ leaseLinkSync?: typeof fs.linkSync;
72
+ afterLifecycleAcquired?: () => void;
73
+ afterReleaseLeasePublished?: (expected: ExpectedDaemon) => void;
74
+ }
75
+ export declare const DAEMON_RELEASE_LEASE_TTL_MS = 30000;
76
+ export interface DaemonLifecycleLock {
77
+ token: string;
78
+ ownerPid: number;
79
+ createdAt: number;
80
+ expiresAt: number;
81
+ }
82
+ /** Atomically acquire the shared startup/release publication arbiter. */
83
+ export declare function tryAcquireDaemonLifecycleLock(root: string, options?: {
84
+ now?: number;
85
+ linkSync?: typeof fs.linkSync;
86
+ }): DaemonLifecycleLock | null;
87
+ export declare function releaseDaemonLifecycleLock(root: string, lock: DaemonLifecycleLock): void;
88
+ export interface DaemonReleaseLease {
89
+ token: string;
90
+ ownerPid: number;
91
+ createdAt: number;
92
+ heartbeatAt: number;
93
+ expiresAt: number;
94
+ generation: Pick<ExpectedDaemon, 'pid' | 'startedAt' | 'socketPath'>;
37
95
  }
96
+ /** True while intentional release exclusively owns this root's lifecycle. */
97
+ export declare function hasActiveDaemonReleaseLease(root: string, now?: number, hooks?: {
98
+ afterRecoveryRead?: () => void;
99
+ }): boolean;
100
+ /** Atomically claim release/cleanup ownership for one daemon generation. */
101
+ export declare function tryAcquireDaemonReleaseLease(root: string, expected: ExpectedDaemon, options?: {
102
+ now?: number;
103
+ linkSync?: typeof fs.linkSync;
104
+ }): DaemonReleaseLease | null;
105
+ /** Publish a complete renewed generation atomically under the recovery marker. */
106
+ export declare function refreshDaemonReleaseLease(root: string, lease: DaemonReleaseLease, now?: number): boolean;
107
+ /** Resolve an explicit project root to the same canonical form daemons use. */
108
+ export declare function canonicalDaemonRoot(root: string): string;
109
+ /** Stable map/set key for aliases of the same canonical root. */
110
+ export declare function canonicalDaemonRootKey(root: string): string;
111
+ /** Project-scoped, idempotent release contract shared by CLI and MCP. */
112
+ export declare function releaseDaemonAt(root: string, deps?: DaemonReleaseDeps): Promise<StopResult>;
38
113
  /**
39
- * Stop the daemon serving `root`: SIGTERM, wait, then SIGKILL if it won't go,
40
- * then sweep its artifacts. `root` must be realpath'd (match how the daemon
41
- * keys its socket/lockfile). Resolves the pid from the authoritative lockfile,
42
- * falling back to the registry.
114
+ * Release the daemon serving `root` through its authenticated socket handshake,
115
+ * wait for confirmed process death, then sweep ownership artifacts. Never sends
116
+ * a signal based only on pidfile/registry liveness. `root` must be realpath'd.
43
117
  */
44
- export declare function stopDaemonAt(root: string): Promise<StopResult>;
118
+ export declare function stopDaemonAt(root: string, deps?: DaemonReleaseDeps): Promise<StopResult>;
119
+ export declare function requestDaemonRelease(expected: ExpectedDaemon): Promise<'releasing' | 'identity-mismatch' | 'unreachable'>;
45
120
  /** Stop every registered, live daemon. */
46
121
  export declare function stopAllDaemons(): Promise<StopResult[]>;
47
122
  //# sourceMappingURL=daemon-registry.d.ts.map
@@ -16,6 +16,7 @@
16
16
  * Spawned detaches (own session/process group) so closing the MCP process
17
17
  * does not take the watchdog down with it.
18
18
  */
19
+ import { type StopResult } from './daemon-registry';
19
20
  export interface DaemonWatchdogOptions {
20
21
  /** Poll interval (ms). Default 30s. */
21
22
  intervalMs?: number;
@@ -33,6 +34,10 @@ export interface DaemonWatchdogOptions {
33
34
  windowsHide?: boolean;
34
35
  stdio?: 'ignore' | ['ignore', number, number];
35
36
  }) => boolean;
37
+ /** Release seam for deterministic overlap/failure tests. */
38
+ releaseFn?: (root: string) => Promise<StopResult>;
39
+ /** Async boundary before spawn; defaults to one microtask for release ordering. */
40
+ beforeSpawn?: () => Promise<void>;
36
41
  }
37
42
  /**
38
43
  * Per-project daemon liveness watchdog. Registers roots; polls every
@@ -40,16 +45,23 @@ export interface DaemonWatchdogOptions {
40
45
  */
41
46
  export declare class DaemonWatchdog {
42
47
  private readonly roots;
48
+ private readonly releasing;
43
49
  private interval;
44
50
  private readonly intervalMs;
45
51
  private readonly scriptPath;
46
52
  private readonly nodePath;
47
53
  private readonly spawnFn;
54
+ private readonly releaseFn;
55
+ private readonly beforeSpawn;
48
56
  constructor(opts?: DaemonWatchdogOptions);
49
57
  /** Watch a project root: if its daemon dies, respawn it. Idempotent. */
50
58
  watch(root: string): void;
59
+ /** Begin a new lifecycle for an intentionally released root. */
60
+ resume(root: string): void;
51
61
  /** Stop watching a root. Idempotent. */
52
62
  unwatch(root: string): void;
63
+ /** Intentionally release one project without allowing the next tick to respawn it. */
64
+ release(root: string): Promise<StopResult>;
53
65
  /** Number of roots currently being watched. */
54
66
  size(): number;
55
67
  /**
@@ -63,8 +63,17 @@ export interface DaemonHello {
63
63
  codegraph: string;
64
64
  pid: number;
65
65
  socketPath: string;
66
+ root: string;
67
+ startedAt: number;
66
68
  protocol: 1;
67
69
  }
70
+ export interface DaemonReleaseControl {
71
+ codegraph_control: 1;
72
+ action: 'release';
73
+ root: string;
74
+ pid: number;
75
+ startedAt: number;
76
+ }
68
77
  /**
69
78
  * Optional reverse-handshake line a proxy sends right after it verifies the
70
79
  * daemon hello, carrying its own pids so the daemon can reap the client if its
@@ -112,9 +121,16 @@ export declare class Daemon {
112
121
  private stopping;
113
122
  private socketPath;
114
123
  private pidPath;
124
+ private startedAt;
125
+ private acquiredGeneration;
126
+ private listenSocket;
127
+ private exit;
115
128
  constructor(projectRoot: string, opts?: {
116
129
  idleTimeoutMs?: number;
117
130
  maxIdleMs?: number;
131
+ generation?: DaemonLockInfo;
132
+ listenSocket?: (socketPath: string, onConnection: (socket: net.Socket) => void) => Promise<net.Server>;
133
+ exit?: (code: number) => void;
118
134
  });
119
135
  /**
120
136
  * Bind the socket, kick off engine init, and register signal handlers. The
@@ -222,7 +238,9 @@ export type AcquireResult = {
222
238
  * widened). The race's worst case is two daemons briefly; on a single external
223
239
  * drive that's strictly better than the daemon never starting at all.
224
240
  */
225
- export declare function tryAcquireDaemonLock(projectRoot: string): AcquireResult;
241
+ export declare function tryAcquireDaemonLock(projectRoot: string, options?: {
242
+ afterLifecycleAcquired?: () => void;
243
+ }): AcquireResult;
226
244
  /**
227
245
  * Exclusive-create the pidfile (O_CREAT|O_EXCL via the `wx` flag) and write the
228
246
  * full record through the same fd — the hard-link-free fallback used by
@@ -285,6 +303,7 @@ export declare function peerIsDead(peers: {
285
303
  pid: number | null;
286
304
  hostPid: number | null;
287
305
  }, isAlive: (pid: number) => boolean): boolean;
306
+ export declare function parseReleaseControlLine(line: string): DaemonReleaseControl | null;
288
307
  /** Exported for test stubs that need to bound the hello-line read. */
289
308
  export { MAX_HELLO_LINE_BYTES };
290
309
  //# sourceMappingURL=daemon.d.ts.map