@ngockhoale/ukit 3.4.29 → 3.4.31
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 +29 -0
- package/manifests/documentation.yaml +100 -0
- package/package.json +1 -1
- package/src/core/executionContracts.js +32 -0
- package/src/core/permissionPolicy.js +1 -1
- package/src/decision/client.js +15 -0
- package/src/decision/protocol.js +12 -14
- package/src/index/buildIndex.js +244 -4
- package/src/index/impactContext.js +91 -11
- package/src/index/languageTools.js +126 -0
- package/src/index/paths.js +5 -1
- package/src/index/playbookRegistry.js +108 -9
- package/src/index/queryIndex.js +415 -1
- package/src/index/routeCatalog.js +1 -1
- package/src/index/routeResolver.js +2 -2
- package/src/index/taskRouting.js +28 -4
- package/template_project/.claude/commands/ukit/handoff-create.md +5 -0
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +43 -4
- package/template_project/.claude/hooks/block-dangerous.mjs +7 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +235 -21
- package/template_project/.claude/hooks/record-execution.mjs +21 -0
- package/template_project/.claude/hooks/skill-router.sh +187 -16
- package/template_project/.claude/settings.json +43 -1
- package/template_project/.claude/ukit/index/cleanup-task-worktree.mjs +1 -1
- package/template_project/.claude/ukit/index/fix-loop-escalation.mjs +10 -4
- package/template_project/.claude/ukit/index/impact-context.mjs +70 -15
- package/template_project/.claude/ukit/index/land-worktree.mjs +2 -2
- package/template_project/.claude/ukit/index/lib/index-core.mjs +883 -14
- package/template_project/.claude/ukit/index/playbook-registry.mjs +108 -9
- package/template_project/.claude/ukit/index/query-index.mjs +100 -7
- package/template_project/.claude/ukit/index/review-verdict.mjs +14 -4
- package/template_project/.claude/ukit/index/route-catalog.mjs +1 -1
- package/template_project/.claude/ukit/index/route-resolver.mjs +34 -2
- package/template_project/.claude/ukit/index/route-task.mjs +243 -38
- package/template_project/.claude/ukit/index/sidecar-decision.mjs +3 -59
- package/template_project/.claude/ukit/index/unic-decision.mjs +27 -14
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +43 -0
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +612 -56
- package/template_project/.claude/ukit/runtime/handoff-intent.mjs +122 -6
- package/template_project/.claude/ukit/runtime/loop-state.mjs +1 -1
- package/template_project/.claude/ukit/runtime/operation-evidence.mjs +87 -0
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +417 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +635 -28
- package/template_project/.omp/hooks/pre/ukit-bridge.js +40 -5
- package/template_project/AGENTS.md +1 -1
- package/template_project/CLAUDE.md +1 -1
- package/template_project/instructions/core.md +1 -1
- package/template_user/playbooks/feature-implementation.md +9 -0
- package/template_user/playbooks/migration.md +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,35 @@ All notable changes to UKit are documented here.
|
|
|
9
9
|
- New `/ukit:ci-check` command + `ci-check.mjs` helper (core pack): prechecks, fail-closed budget guard, local preflight, at most one dispatch per invocation, write-ahead run-id state, `status` (never dispatches), `--ref`, `--dry-run`, per-OS report. Never pushes, merges or publishes.
|
|
10
10
|
- Default-branch bootstrap stub documented in `docs/design/CI-CROSS-PLATFORM-MATRIX.md` (requires separate approval).
|
|
11
11
|
|
|
12
|
+
## 3.4.31 - 2026-10-11
|
|
13
|
+
|
|
14
|
+
**Completion Gate: task-aware operational completion (fixes the CronDelete/CronList false positive).** Verified with the full suite (`yarn test`, 566 files) plus focused six-scenario routing → recorder → Stop regressions.
|
|
15
|
+
- **Task kinds.** CODE_IMPLEMENTATION / OPERATIONAL / READ_ONLY / CONFIGURATION / MIXED are classified independently of write activity. Implementation still requires a source write plus risk-based verification; mixed tasks keep both obligations.
|
|
16
|
+
- **Operational evidence.** New bounded, tool-agnostic proof bank (`operation-evidence.mjs`) and strict native-result normalization (`cron-operation-adapter.mjs`): completion needs an accepted execution, matching target/scope, a fresh verified postcondition and active-task evidence. Unsupported result shapes fail closed.
|
|
17
|
+
- **Task-instance isolation.** Each request gets its own task identity; identical repeated requests are distinct tasks, while continue/resume keeps identity. Evidence and journals from an earlier task cannot satisfy a new one.
|
|
18
|
+
- **Receipt retention / replay protection.** Failed operations survive receipt eviction; native tool-use IDs are tracked (bounded, fail-closed on saturation) so an overwritten observation cannot replay as fresh proof.
|
|
19
|
+
- **Claude Code / OMP.** The Claude recorder and settings matchers cover `CronDelete`/`CronList` success and failure events; the OMP bridge normalizes `cron_delete`/`cron_list` results. Codex uses the shared policy helper (no native Stop enforcement claimed).
|
|
20
|
+
- **Negation-aware classification.** "Xóa cron …, không thay đổi code, Git hoặc Docker" / "Delete cron …. Do not modify source code, Git or Docker." classify as OPERATIONAL; negated source instructions no longer become source requirements.
|
|
21
|
+
- **No-route mutation.** A prompt that ordered a change but matched no skill (no routed contract) now ends with an explicit "nothing was verified" notice instead of a silent stop. This is a notice, not a block — a known limitation.
|
|
22
|
+
- Loop-breaker release note no longer claims no verification ran; docs retry-guidance wording aligned in the template instructions.
|
|
23
|
+
- Limits: classifier is lexical; the Cron adapter does not support an "ensure presence" goal (fails closed); no live native end-to-end run was performed.
|
|
24
|
+
|
|
25
|
+
## 3.4.30 - 2026-10-10
|
|
26
|
+
|
|
27
|
+
**Orchestration: routing, goal/cycle continuation, task ownership, discovery, landing.** Verified with `yarn test:handoff` (184/184) and `yarn release:verify`; live Claude Code two-cycle run-all and the Claude create-only permission denial were exercised on disposable fixtures. Known limits: OMP permission denial is verified through the hook bridge only, and Codex has no pre-tool hook for ownership enforcement. No performance gain is claimed beyond removing the identified redundant Stop-denial / archive-repair round.
|
|
28
|
+
- **Stop-gate evidence capture (P0).** `isVerificationCommand`/`TEST_RUNNER_COMMAND` now accept subshell/group openers, so `(yarn test; echo "exit $?" >> log)` mints a receipt (it previously minted none → endless `missing verification-evidence`). `verdictHistory` retention never evicts the last satisfying PASS; a pipe-masked run is reported as "exit status unproven" instead of "fix the failure". Pipe-masked status is still rejected. Tests: `tests/hooks/stopGateReceiptCapture.test.js`.
|
|
29
|
+
- **F3 ↔ ExitPredicate.** `index:all-done` on a header-only INDEX now reads `archive/cycle-NN/TASK-*.md`; `run-all` additionally requires no pending queued blueprints. Finalizing a cycle never implies run-all completion. Test: `tests/handoff/f3FinalizationPredicate.test.mjs`.
|
|
30
|
+
- **Archive/Goal completion invariant.** F3 now runs `resumable-run.mjs archive-cycle <root> <NN>` before the INDEX reset: the verified INDEX rows are persisted to `archive/cycle-NN/INDEX.md` and terminal statuses stamped into archived task headers; `index:all-done` reads that snapshot (header only for legacy archives, missing row fails closed, rows immutable per cycle). Removes the stale-`ready` repair round. Receipt detection ignores quoted literals and sees `bash -c '<runner>'`. Tests: `archiveCompletionInvariant.test.mjs` (AR-01…15).
|
|
31
|
+
- **Main-agent task ownership.** During implement phases the main agent must hold a claim (`main:<session>` owner) via the existing registry; new CLI `resumable-run.mjs claim|release <root> <TASK-ID> --owner <owner>`.
|
|
32
|
+
- **Routing.** Deterministic greeting fast-path; Vietnamese edit verbs; REFERENCE / CLONE / SQL / REUSE / RUN-ALL signals stamped in the route summary; negated and plan-only prompts never imply implementation; "run all the tests" is a test run, "chạy hết pending tasks" is goal-level run-all; "use <symbol> from Core" stamps reuse-first. Explicit handoff commands keep authority over inferred intent.
|
|
33
|
+
- **Goal vs cycle.** `deriveGoalStatus`; default exit predicate `all(index:all-done)`; quota pause marks `Paused: yes` (structured error records only); a finalized cycle (done cursor + header-only INDEX) releases once.
|
|
34
|
+
- **Task ownership.** Per-task claims with task identity, lease renewal, owner fencing and takeover; wired through the Claude Code and OMP delegation hooks; per-session route-state sidecars fix a cross-session collision. The main agent takes part in the same registry (see above).
|
|
35
|
+
- **Discovery.** Index schema 9: fuzzy symbol candidates (never auto-substituted), global and SQL symbols, caller proof with `unverifiedImpact`, bounded fallback search that reports truncation instead of posing as exhaustive.
|
|
36
|
+
- **Reuse/reference/clone/SQL advisory.** `skill-router.sh` now emits one short source-resolution line for reuse-first, reference/clone and SQL prompts (including the previously silent no-route exit): resolve the named symbol with `query-index.mjs --symbol`, never invent a module, leave the reference untouched, alias vs real column. Found by a live OMP run that invented `import … from 'Core'`. `REFERENCE_TARGET_RE` also accepts "like the reviewed Customer dropdown".
|
|
37
|
+
- **`handoff-fullstack` F3.** Archive step now snapshots verified INDEX rows (`archive-cycle`) so a finished cycle never needs a post-`done` repair round; the final verification for `verify:pass` runs in-session (the receipt is per-session).
|
|
38
|
+
- **Playbook observability.** `ledger.playbookTrace`, `--playbook-status`.
|
|
39
|
+
- **Fixes.** `land-worktree` / `cleanup-task-worktree` accepted only `TASK-C97-NNN`, so every ordinary handoff task (`TASK-001`) was blocked at landing; they now accept `TASK-NNN` and `TASK-<cycle>-NNN` (still no separators or whitespace). `block-dangerous` no longer blocks a quoted-delimiter heredoc whose body merely documents a dangerous command; real commands, unquoted heredocs and heredocs piped to a shell stay denied. `handoff-fullstack` P3 no longer blocks the run when the lite commit agent has no shell. `handoff-create` headless rule.
|
|
40
|
+
|
|
12
41
|
## 3.4.29 - 2026-10-09
|
|
13
42
|
|
|
14
43
|
**Fixes (no new features).**
|
|
@@ -418,6 +418,94 @@ entries:
|
|
|
418
418
|
validation: [none]
|
|
419
419
|
archive_policy: immutable
|
|
420
420
|
notes: C99 measurement-only sub-agent cost audit snapshot; registered in 3.4.25 (was unclassified on 3.4.24)
|
|
421
|
+
- id: docs-analysis-handoff-root-cause-audit
|
|
422
|
+
path: docs/analysis/HANDOFF-ROOT-CAUSE-AUDIT.md
|
|
423
|
+
class: archive
|
|
424
|
+
audience: [maintainer]
|
|
425
|
+
owner: product
|
|
426
|
+
source_of_truth: docs/analysis/HANDOFF-ROOT-CAUSE-AUDIT.md
|
|
427
|
+
merge_strategy: none
|
|
428
|
+
load_policy: never
|
|
429
|
+
validation: [none]
|
|
430
|
+
archive_policy: immutable
|
|
431
|
+
notes: 2026-10-09 handoff root-cause audit (create-scope authority + Stop verification semantics)
|
|
432
|
+
- id: docs-analysis-handoff-remediation-plan
|
|
433
|
+
path: docs/analysis/HANDOFF-REMEDIATION-PLAN.md
|
|
434
|
+
class: archive
|
|
435
|
+
audience: [maintainer]
|
|
436
|
+
owner: product
|
|
437
|
+
source_of_truth: docs/analysis/HANDOFF-REMEDIATION-PLAN.md
|
|
438
|
+
merge_strategy: none
|
|
439
|
+
load_policy: never
|
|
440
|
+
validation: [none]
|
|
441
|
+
archive_policy: immutable
|
|
442
|
+
notes: 2026-10-09 handoff remediation plan (create-scope authority + Stop verification semantics)
|
|
443
|
+
- id: docs-analysis-handoff-regression-test-plan
|
|
444
|
+
path: docs/analysis/HANDOFF-REGRESSION-TEST-PLAN.md
|
|
445
|
+
class: archive
|
|
446
|
+
audience: [maintainer]
|
|
447
|
+
owner: product
|
|
448
|
+
source_of_truth: docs/analysis/HANDOFF-REGRESSION-TEST-PLAN.md
|
|
449
|
+
merge_strategy: none
|
|
450
|
+
load_policy: never
|
|
451
|
+
validation: [none]
|
|
452
|
+
archive_policy: immutable
|
|
453
|
+
notes: 2026-10-09 handoff regression test plan (create-scope authority + Stop verification semantics)
|
|
454
|
+
- id: docs-analysis-orchestration-root-cause-audit
|
|
455
|
+
path: docs/analysis/ORCHESTRATION-ROOT-CAUSE-AUDIT.md
|
|
456
|
+
class: archive
|
|
457
|
+
audience: [maintainer]
|
|
458
|
+
owner: product
|
|
459
|
+
source_of_truth: docs/analysis/ORCHESTRATION-ROOT-CAUSE-AUDIT.md
|
|
460
|
+
merge_strategy: none
|
|
461
|
+
load_policy: never
|
|
462
|
+
validation: [none]
|
|
463
|
+
archive_policy: immutable
|
|
464
|
+
notes: 2026-10-10 orchestration root-cause audit (routing, playbooks, discovery, goal cycles)
|
|
465
|
+
- id: docs-analysis-orchestration-spec
|
|
466
|
+
path: docs/analysis/ORCHESTRATION-SPEC.md
|
|
467
|
+
class: archive
|
|
468
|
+
audience: [maintainer]
|
|
469
|
+
owner: product
|
|
470
|
+
source_of_truth: docs/analysis/ORCHESTRATION-SPEC.md
|
|
471
|
+
merge_strategy: none
|
|
472
|
+
load_policy: never
|
|
473
|
+
validation: [none]
|
|
474
|
+
archive_policy: immutable
|
|
475
|
+
notes: 2026-10-10 orchestration behavior spec
|
|
476
|
+
- id: docs-analysis-orchestration-implementation-plan
|
|
477
|
+
path: docs/analysis/ORCHESTRATION-IMPLEMENTATION-PLAN.md
|
|
478
|
+
class: archive
|
|
479
|
+
audience: [maintainer]
|
|
480
|
+
owner: product
|
|
481
|
+
source_of_truth: docs/analysis/ORCHESTRATION-IMPLEMENTATION-PLAN.md
|
|
482
|
+
merge_strategy: none
|
|
483
|
+
load_policy: never
|
|
484
|
+
validation: [none]
|
|
485
|
+
archive_policy: immutable
|
|
486
|
+
notes: 2026-10-10 orchestration implementation plan
|
|
487
|
+
- id: docs-analysis-orchestration-regression-tests
|
|
488
|
+
path: docs/analysis/ORCHESTRATION-REGRESSION-TESTS.md
|
|
489
|
+
class: archive
|
|
490
|
+
audience: [maintainer]
|
|
491
|
+
owner: product
|
|
492
|
+
source_of_truth: docs/analysis/ORCHESTRATION-REGRESSION-TESTS.md
|
|
493
|
+
merge_strategy: none
|
|
494
|
+
load_policy: never
|
|
495
|
+
validation: [none]
|
|
496
|
+
archive_policy: immutable
|
|
497
|
+
notes: 2026-10-10 orchestration regression test inventory
|
|
498
|
+
- id: docs-analysis-orchestration-final-verification
|
|
499
|
+
path: docs/analysis/ORCHESTRATION-FINAL-VERIFICATION.md
|
|
500
|
+
class: archive
|
|
501
|
+
audience: [maintainer]
|
|
502
|
+
owner: product
|
|
503
|
+
source_of_truth: docs/analysis/ORCHESTRATION-FINAL-VERIFICATION.md
|
|
504
|
+
merge_strategy: none
|
|
505
|
+
load_policy: never
|
|
506
|
+
validation: [none]
|
|
507
|
+
archive_policy: immutable
|
|
508
|
+
notes: 2026-10-10 orchestration final verification and release readiness
|
|
421
509
|
- id: docs-analysis-c100-frag-a-d
|
|
422
510
|
path: docs/analysis/c100/frag-A-D.md
|
|
423
511
|
class: archive
|
|
@@ -845,6 +933,18 @@ entries:
|
|
|
845
933
|
archive_policy: never
|
|
846
934
|
notes: C69 G7 operator doc — timeline render, support bundle contents/exclusions, replay limits
|
|
847
935
|
|
|
936
|
+
- id: docs-unic-decision-integration-p1-fix
|
|
937
|
+
path: docs/UNIC-DECISION-INTEGRATION-P1-FIX.md
|
|
938
|
+
class: canonical
|
|
939
|
+
audience: [maintainer]
|
|
940
|
+
owner: runtime
|
|
941
|
+
source_of_truth: docs/UNIC-DECISION-INTEGRATION-P1-FIX.md
|
|
942
|
+
merge_strategy: none
|
|
943
|
+
load_policy: on-demand
|
|
944
|
+
validation: [manual]
|
|
945
|
+
archive_policy: never
|
|
946
|
+
notes: P1 safety-floor / ordinal choice score / request-cap fix record for the unic-decision integration (9971eac0)
|
|
947
|
+
|
|
848
948
|
- id: docs-design-ci-cross-platform-matrix
|
|
849
949
|
path: docs/design/CI-CROSS-PLATFORM-MATRIX.md
|
|
850
950
|
class: canonical
|
package/package.json
CHANGED
|
@@ -166,3 +166,35 @@ export function buildConfigContracts() {
|
|
|
166
166
|
Object.entries(EXECUTION_CONTRACTS).map(([mode, contract]) => [mode, stripContractForConfig(contract)])
|
|
167
167
|
);
|
|
168
168
|
}
|
|
169
|
+
|
|
170
|
+
// Task semantics are independent of source execution strategy and observed progress.
|
|
171
|
+
export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
172
|
+
const text = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').toLowerCase();
|
|
173
|
+
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
174
|
+
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
175
|
+
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
176
|
+
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
177
|
+
&& /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
|
|
178
|
+
const operation = actionable.some((clause) => /\b(?:delete|remove|cancel|xoa|huy|ensure|dam bao)\b/.test(clause)
|
|
179
|
+
&& /\bcron\b|scheduled\s+(?:cron\s+)?(?:task|job)|tac vu\s+(?:cron|dinh ky)/.test(clause));
|
|
180
|
+
const config = actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh)\b/.test(clause));
|
|
181
|
+
const repository = Boolean(targetFile) || /[\w.-]+\.(?:json|ya?ml|toml|ini)\b/.test(text);
|
|
182
|
+
const readOnly = /\b(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(text)
|
|
183
|
+
&& !sourceOrder && !operation && !config;
|
|
184
|
+
const taskKind = operation ? (sourceOrder ? 'MIXED' : 'OPERATIONAL')
|
|
185
|
+
: config ? 'CONFIGURATION' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
186
|
+
const id = text.match(/\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/)?.[0] || null;
|
|
187
|
+
return { taskKind, ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
|
|
188
|
+
...(operation ? { operationGoal: { domain: 'scheduled-job', target: id, desiredState: /\b(?:delete|remove|cancel|xoa|huy|absent|removed|deleted|no longer|khong con)\b/.test(actionable.join(' ')) ? 'absent' : 'present',
|
|
189
|
+
completionSemantics: /\b(?:ensure|dam bao)\b/.test(text) ? 'ensure-state' : 'execute-and-verify' } } : {}) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function applyTaskSemantics(contract, semantic) {
|
|
193
|
+
const evidence = contract?.completionEvidence || [];
|
|
194
|
+
const operationOnly = semantic.taskKind === 'OPERATIONAL'
|
|
195
|
+
|| (semantic.taskKind === 'CONFIGURATION' && semantic.configurationScope === 'external');
|
|
196
|
+
return { ...(contract || {}), ...semantic,
|
|
197
|
+
completionEvidence: operationOnly ? ['operation-verification']
|
|
198
|
+
: semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
|
|
199
|
+
: semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
|
|
200
|
+
}
|
|
@@ -36,7 +36,7 @@ export const CANONICAL_DENY = Object.freeze([
|
|
|
36
36
|
{ id: 'rm-rf-parent', claude: 'Bash(rm -rf ..:*)', omp: 'rm -rf ..', codex: 'rm -rf ..', hardDeny: true },
|
|
37
37
|
{ id: 'rm-rf-dot', claude: 'Bash(rm -rf .:*)', omp: 'rm -rf .', codex: 'rm -rf .', hardDeny: true },
|
|
38
38
|
{ id: 'dd-raw-device', claude: 'Bash(dd if=/dev/:*)', omp: 'dd if=/dev/*', codex: 'dd if=/dev/', hardDeny: true },
|
|
39
|
-
{ id: 'write-raw-device', claude: 'Bash(*> /dev/sda
|
|
39
|
+
{ id: 'write-raw-device', claude: 'Bash(*> /dev/sda*)', omp: '*> /dev/sda*', codex: '> /dev/sda', hardDeny: true },
|
|
40
40
|
{ id: 'mkfs', claude: 'Bash(mkfs:*)', omp: 'mkfs.*', codex: 'mkfs', hardDeny: true },
|
|
41
41
|
{ id: 'fork-bomb', claude: 'Bash(:(){ :|:& };::*)', omp: ':(){ :|:& };:*', codex: ':(){ :|:& };:', hardDeny: true },
|
|
42
42
|
]);
|
package/src/decision/client.js
CHANGED
|
@@ -368,6 +368,21 @@ export function createDecisionClient({
|
|
|
368
368
|
return outcome('invalid', { fallbackCode: 'empty-batch' });
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
const maxBatch = dp.maxBatchQuestions ?? 8;
|
|
372
|
+
const maxEnum = dp.maxEnumLabels ?? 11;
|
|
373
|
+
if (![maxBatch, maxEnum].every(n => Number.isInteger(n) && n > 0)) {
|
|
374
|
+
return outcome('unsupported', { fallbackCode: 'invalid-decision-limits' });
|
|
375
|
+
}
|
|
376
|
+
if (questions.length > maxBatch) {
|
|
377
|
+
return outcome('unsupported', { fallbackCode: 'max-batch-questions' });
|
|
378
|
+
}
|
|
379
|
+
if (questions.some(q => Array.isArray(q.candidates) && q.candidates.length > maxEnum)) {
|
|
380
|
+
return outcome('unsupported', { fallbackCode: 'max-enum-labels' });
|
|
381
|
+
}
|
|
382
|
+
if (new Set(questions.map(q => q.decisionKey)).size !== questions.length) {
|
|
383
|
+
return outcome('invalid', { fallbackCode: 'duplicate-question' });
|
|
384
|
+
}
|
|
385
|
+
|
|
371
386
|
// Language classification covers state + question text; the English
|
|
372
387
|
// checkpoint is reachable only when the whole request proves English.
|
|
373
388
|
// Malformed batch input (cyclic state, non-serializable values, internal
|
package/src/decision/protocol.js
CHANGED
|
@@ -51,23 +51,20 @@ export function encodeQuestion(question) {
|
|
|
51
51
|
description: 'true iff the proposition holds; interpreted as P(true).',
|
|
52
52
|
};
|
|
53
53
|
} else {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
value.minimum = 1;
|
|
59
|
-
value.maximum = legend.length;
|
|
60
|
-
value.description = `Ordered scale: ${legend
|
|
61
|
-
.map((label, i) => `${i + 1}=${label}`)
|
|
62
|
-
.join(', ')}.`;
|
|
54
|
+
if (!Array.isArray(candidates) || candidates.length === 0
|
|
55
|
+
|| candidates.some(label => typeof label !== 'string' || !label.trim())
|
|
56
|
+
|| new Set(candidates).size !== candidates.length) {
|
|
57
|
+
throw new TypeError('score requires unique non-empty ordinal labels.');
|
|
63
58
|
}
|
|
59
|
+
value = { type: 'string', enum: [...candidates] };
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
return {
|
|
67
63
|
type: 'function',
|
|
68
64
|
function: {
|
|
69
65
|
name: decisionKey,
|
|
70
|
-
description: String(instruction ?? '')
|
|
66
|
+
description: String(instruction ?? '') + (kind === 'score'
|
|
67
|
+
? `\nOrdered ordinal rubric: ${candidates.map((label, i) => `${i + 1}=${JSON.stringify(label)}`).join(', ')}. Choose exactly one label; do not return an expected numeric score.` : ''),
|
|
71
68
|
parameters: {
|
|
72
69
|
type: 'object',
|
|
73
70
|
properties: { value },
|
|
@@ -184,11 +181,12 @@ function validateValue(question, args) {
|
|
|
184
181
|
if (isFiniteNumber(value) && value >= 0 && value <= 1) return { value };
|
|
185
182
|
return 'missing-value';
|
|
186
183
|
}
|
|
187
|
-
//
|
|
188
|
-
if (!isFiniteNumber(value) || !Number.isInteger(value)) return 'missing-value';
|
|
184
|
+
// Decode an explicit choice losslessly; never reinterpret native expectations.
|
|
189
185
|
const legend = Array.isArray(question.candidates) ? question.candidates : [];
|
|
190
|
-
if (legend.length
|
|
191
|
-
|
|
186
|
+
if (!legend.length || new Set(legend).size !== legend.length) return 'out-of-enum';
|
|
187
|
+
if (typeof value !== 'string') return 'missing-value';
|
|
188
|
+
const ordinal = legend.indexOf(value);
|
|
189
|
+
return ordinal < 0 ? 'out-of-enum' : { value: ordinal + 1 };
|
|
192
190
|
}
|
|
193
191
|
|
|
194
192
|
/**
|
package/src/index/buildIndex.js
CHANGED
|
@@ -1681,7 +1681,7 @@ function computeSignature(content, lineStartIndex) {
|
|
|
1681
1681
|
export function extractSymbols(filePath, content) {
|
|
1682
1682
|
const symbols = [];
|
|
1683
1683
|
const lineStarts = computeLineStarts(content);
|
|
1684
|
-
const addSymbol = (name, type, matchIndex = null) => {
|
|
1684
|
+
const addSymbol = (name, type, matchIndex = null, extra = null) => {
|
|
1685
1685
|
if (!name) {
|
|
1686
1686
|
return;
|
|
1687
1687
|
}
|
|
@@ -1691,7 +1691,7 @@ export function extractSymbols(filePath, content) {
|
|
|
1691
1691
|
}
|
|
1692
1692
|
const line = lineNumberForIndex(lineStarts, matchIndex);
|
|
1693
1693
|
const signature = computeSignature(content, lineStarts[line - 1]);
|
|
1694
|
-
symbols.push({ name, type, filePath, line, signature });
|
|
1694
|
+
symbols.push(extra ? { name, type, filePath, line, signature, ...extra } : { name, type, filePath, line, signature });
|
|
1695
1695
|
};
|
|
1696
1696
|
const addMatches = (regex, type, groupIndex = 1) => {
|
|
1697
1697
|
for (const match of content.matchAll(regex)) {
|
|
@@ -1713,7 +1713,26 @@ export function extractSymbols(filePath, content) {
|
|
|
1713
1713
|
addMatches(new RegExp(`CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'index');
|
|
1714
1714
|
addMatches(new RegExp(`CREATE\\s+(?:TYPE|DOMAIN|SEQUENCE|MATERIALIZED\\s+VIEW)\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'type');
|
|
1715
1715
|
addMatches(new RegExp(`ALTER\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'table');
|
|
1716
|
-
|
|
1716
|
+
|
|
1717
|
+
// Column-level symbols: CREATE TABLE bodies (with parent + schema kept so
|
|
1718
|
+
// same-named columns in different schemas/tables stay distinct) and view
|
|
1719
|
+
// output aliases (`expr AS alias`).
|
|
1720
|
+
const sqlObjects = findSqlObjects(content);
|
|
1721
|
+
for (const [objectIndex, sqlObject] of sqlObjects.entries()) {
|
|
1722
|
+
if (sqlObject.type === 'table') {
|
|
1723
|
+
for (const column of extractSqlTableColumns(content, sqlObject)) {
|
|
1724
|
+
addSymbol(column.name, 'column', column.matchIndex, { parent: sqlObject.name, schema: sqlObject.schema });
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
if (sqlObject.type === 'view') {
|
|
1728
|
+
const bodyEnd = objectIndex + 1 < sqlObjects.length ? sqlObjects[objectIndex + 1].matchIndex : content.length;
|
|
1729
|
+
for (const alias of extractSqlViewAliases(content.slice(sqlObject.matchIndex, Math.min(bodyEnd, sqlObject.matchIndex + 20000)))) {
|
|
1730
|
+
addSymbol(alias.name, 'column', sqlObject.matchIndex + alias.matchIndex, { parent: sqlObject.name, schema: sqlObject.schema });
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
return dedupeBy(symbols, (item) => `${item.filePath}:${item.type}:${item.name}:${item.parent ?? ''}:${item.schema ?? ''}`);
|
|
1717
1736
|
}
|
|
1718
1737
|
|
|
1719
1738
|
addMatches(/export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'function');
|
|
@@ -1727,6 +1746,32 @@ export function extractSymbols(filePath, content) {
|
|
|
1727
1746
|
addMatches(/defineComponent\(\s*\{[\s\S]*?name\s*:\s*['"]([^'"]+)['"]/g, 'component-name');
|
|
1728
1747
|
addMatches(/export\s+default\s*\{[\s\S]*?name\s*:\s*['"]([^'"]+)['"]/g, 'component-name');
|
|
1729
1748
|
|
|
1749
|
+
// Global-registered frontend functions: reachable from anywhere without an
|
|
1750
|
+
// import edge, so impact analysis must treat them as verified dependencies.
|
|
1751
|
+
addMatches(/\b(?:window|globalThis|global)\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*=/g, 'global');
|
|
1752
|
+
addMatches(/\b(?:window|globalThis|global)\s*\[\s*['"]([^'"]+)['"]\s*\]\s*=/g, 'global');
|
|
1753
|
+
addMatches(/\b(?:app|vueApp)\s*\.\s*config\s*\.\s*globalProperties\s*\.\s*(\$?[A-Za-z_$][A-Za-z0-9_$]*)\s*=/g, 'global');
|
|
1754
|
+
addMatches(/\bVue\s*\.\s*prototype\s*\.\s*(\$?[A-Za-z_$][A-Za-z0-9_$]*)\s*=/g, 'global');
|
|
1755
|
+
addMatches(/\b(?:app|vueApp)\s*\.\s*provide\s*\(\s*['"]([^'"]+)['"]/g, 'global');
|
|
1756
|
+
addMatches(/\b(?:Vue|app|vueApp)\s*\.\s*component\s*\(\s*['"]([^'"]+)['"]/g, 'global');
|
|
1757
|
+
|
|
1758
|
+
// Vue options-API methods: `methods: { submit() {...}, validate: () => ... }`.
|
|
1759
|
+
// Names inside the balanced `methods` block only — body-level matches keep
|
|
1760
|
+
// their real file line through the absolute-index offset.
|
|
1761
|
+
for (const methodsMatch of content.matchAll(/\bmethods\s*:\s*\{/g)) {
|
|
1762
|
+
const methodsBody = sliceBalancedBlock(content, methodsMatch.index + methodsMatch[0].length);
|
|
1763
|
+
if (!methodsBody) {
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
const methodsStart = methodsMatch.index + methodsMatch[0].length;
|
|
1767
|
+
for (const methodMatch of methodsBody.matchAll(/(?:^|[,{\n]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*\((?:[^()]|\([^)]*\))*\)\s*\{/g)) {
|
|
1768
|
+
addSymbol(methodMatch[1], 'method', methodsStart + methodMatch.index + methodMatch[0].indexOf(methodMatch[1]));
|
|
1769
|
+
}
|
|
1770
|
+
for (const methodMatch of methodsBody.matchAll(/(?:^|[,{]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:\s*(?:async\s+)?(?:function\b|\([^)]*\)\s*=>|[A-Za-z_$][A-Za-z0-9_$]*\s*=>)/g)) {
|
|
1771
|
+
addSymbol(methodMatch[1], 'method', methodsStart + methodMatch.index + methodMatch[0].indexOf(methodMatch[1]));
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1730
1775
|
if (/\bexport\s+default\b/.test(content)) {
|
|
1731
1776
|
addSymbol('default', 'default-export');
|
|
1732
1777
|
}
|
|
@@ -1743,7 +1788,120 @@ export function extractSymbols(filePath, content) {
|
|
|
1743
1788
|
}
|
|
1744
1789
|
}
|
|
1745
1790
|
|
|
1746
|
-
return dedupeBy(symbols, (item) => `${item.filePath}:${item.type}:${item.name}`);
|
|
1791
|
+
return dedupeBy(symbols, (item) => `${item.filePath}:${item.type}:${item.name}:${item.parent ?? ''}:${item.schema ?? ''}`);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Locates every CREATE <object> statement with its schema qualifier (or null)
|
|
1795
|
+
// so same-named objects in different schemas stay distinct records. Used for
|
|
1796
|
+
// column extraction and SQL reference segmentation.
|
|
1797
|
+
function findSqlObjects(content) {
|
|
1798
|
+
const sqlQualifiedIdent = '(?:(["`]?[A-Za-z_][A-Za-z0-9_$]*["`]?)\\s*\\.\\s*)?["`]?([A-Za-z_][A-Za-z0-9_$]*)["`]?';
|
|
1799
|
+
const patterns = [
|
|
1800
|
+
['table', `CREATE\\s+(?:OR\\s+REPLACE\\s+)?TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlQualifiedIdent}`],
|
|
1801
|
+
['view', `CREATE\\s+(?:OR\\s+REPLACE\\s+)?VIEW\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlQualifiedIdent}`],
|
|
1802
|
+
['function', `CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlQualifiedIdent}`],
|
|
1803
|
+
['procedure', `CREATE\\s+(?:OR\\s+REPLACE\\s+)?PROCEDURE\\s+${sqlQualifiedIdent}`],
|
|
1804
|
+
['type', `CREATE\\s+(?:TYPE|DOMAIN|SEQUENCE)\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlQualifiedIdent}`],
|
|
1805
|
+
];
|
|
1806
|
+
const objects = [];
|
|
1807
|
+
for (const [type, pattern] of patterns) {
|
|
1808
|
+
for (const match of content.matchAll(new RegExp(pattern, 'gi'))) {
|
|
1809
|
+
if (!match[2]) {
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
objects.push({
|
|
1813
|
+
type,
|
|
1814
|
+
name: match[2],
|
|
1815
|
+
schema: match[1] ? match[1].replace(/["`]/g, '') : null,
|
|
1816
|
+
matchIndex: match.index,
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return objects.sort((a, b) => a.matchIndex - b.matchIndex);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
const SQL_COLUMN_CONSTRAINT_KEYWORDS = new Set([
|
|
1824
|
+
'primary', 'foreign', 'unique', 'check', 'constraint', 'key', 'index', 'exclude', 'like', 'period',
|
|
1825
|
+
]);
|
|
1826
|
+
|
|
1827
|
+
// Slices a CREATE TABLE body's balanced paren list and emits one row per
|
|
1828
|
+
// top-level column definition (constraint rows are skipped).
|
|
1829
|
+
function extractSqlTableColumns(content, sqlObject) {
|
|
1830
|
+
const headerPattern = new RegExp(
|
|
1831
|
+
`CREATE\\s+(?:OR\\s+REPLACE\\s+)?TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(?:(?:["\`]?[A-Za-z_][A-Za-z0-9_$]*["\`]?)\\s*\\.\\s*)?["\`]?${escapeRegExp(sqlObject.name)}["\`]?`,
|
|
1832
|
+
'gi',
|
|
1833
|
+
);
|
|
1834
|
+
headerPattern.lastIndex = sqlObject.matchIndex;
|
|
1835
|
+
const headerMatch = headerPattern.exec(content);
|
|
1836
|
+
if (!headerMatch) {
|
|
1837
|
+
return [];
|
|
1838
|
+
}
|
|
1839
|
+
const openParenSearchEnd = Math.min(content.length, headerMatch.index + headerMatch[0].length + 500);
|
|
1840
|
+
const openParen = content.slice(headerMatch.index + headerMatch[0].length - 1, openParenSearchEnd).indexOf('(');
|
|
1841
|
+
if (openParen === -1) {
|
|
1842
|
+
return [];
|
|
1843
|
+
}
|
|
1844
|
+
const openParenAbsolute = headerMatch.index + headerMatch[0].length - 1 + openParen;
|
|
1845
|
+
|
|
1846
|
+
let depth = 0;
|
|
1847
|
+
let closeParen = -1;
|
|
1848
|
+
for (let index = openParenAbsolute; index < content.length; index += 1) {
|
|
1849
|
+
if (content[index] === '(') {
|
|
1850
|
+
depth += 1;
|
|
1851
|
+
} else if (content[index] === ')') {
|
|
1852
|
+
depth -= 1;
|
|
1853
|
+
if (depth === 0) {
|
|
1854
|
+
closeParen = index;
|
|
1855
|
+
break;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
if (closeParen === -1) {
|
|
1860
|
+
return [];
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
const body = content.slice(openParenAbsolute + 1, closeParen);
|
|
1864
|
+
const columns = [];
|
|
1865
|
+
let topLevelDepth = 0;
|
|
1866
|
+
let segmentStart = 0;
|
|
1867
|
+
const segments = [];
|
|
1868
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
1869
|
+
const char = body[index];
|
|
1870
|
+
if (char === '(') {
|
|
1871
|
+
topLevelDepth += 1;
|
|
1872
|
+
} else if (char === ')') {
|
|
1873
|
+
topLevelDepth -= 1;
|
|
1874
|
+
} else if (char === ',' && topLevelDepth === 0) {
|
|
1875
|
+
segments.push(body.slice(segmentStart, index));
|
|
1876
|
+
segmentStart = index + 1;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
segments.push(body.slice(segmentStart));
|
|
1880
|
+
|
|
1881
|
+
let offset = openParenAbsolute + 1;
|
|
1882
|
+
for (const segment of segments) {
|
|
1883
|
+
const columnMatch = segment.match(/^\s*["`]?([A-Za-z_][A-Za-z0-9_$]*)["`]?/);
|
|
1884
|
+
const columnName = columnMatch?.[1];
|
|
1885
|
+
if (columnName && !SQL_COLUMN_CONSTRAINT_KEYWORDS.has(columnName.toLowerCase())) {
|
|
1886
|
+
columns.push({ name: columnName, matchIndex: offset + columnMatch[0].indexOf(columnMatch[1]) });
|
|
1887
|
+
}
|
|
1888
|
+
offset += segment.length + 1;
|
|
1889
|
+
}
|
|
1890
|
+
return columns;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// Output aliases inside a CREATE VIEW body segment: `expr AS alias` where the
|
|
1894
|
+
// alias is followed by a column-list boundary.
|
|
1895
|
+
function extractSqlViewAliases(viewSegment) {
|
|
1896
|
+
const aliases = [];
|
|
1897
|
+
for (const match of viewSegment.matchAll(/\bAS\s+["`]?([A-Za-z_][A-Za-z0-9_$]*)["`]?\s*(?=\s*(?:,|FROM|WHERE|;|\n|$))/gi)) {
|
|
1898
|
+
aliases.push({ name: match[1], matchIndex: match.index + match[0].indexOf(match[1]) });
|
|
1899
|
+
}
|
|
1900
|
+
return aliases;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function escapeRegExp(value) {
|
|
1904
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1747
1905
|
}
|
|
1748
1906
|
|
|
1749
1907
|
function extractImports(filePath, content) {
|
|
@@ -1912,7 +2070,89 @@ function sliceBalancedBlock(content, startIndex) {
|
|
|
1912
2070
|
return '';
|
|
1913
2071
|
}
|
|
1914
2072
|
|
|
2073
|
+
// SQL keywords that appear before `(` in procedural/plain SQL but are not
|
|
2074
|
+
// object references (SELECT count(, IF(, CAST(...) — plus common column type
|
|
2075
|
+
// names, since `VARCHAR(20)` in a table body is a type, not a call.
|
|
2076
|
+
const SQL_CALL_KEYWORDS = new Set([
|
|
2077
|
+
'select', 'from', 'where', 'and', 'or', 'not', 'case', 'when', 'then', 'else',
|
|
2078
|
+
'end', 'join', 'inner', 'left', 'right', 'outer', 'full', 'cross', 'on',
|
|
2079
|
+
'group', 'order', 'by', 'having', 'limit', 'offset', 'union', 'all',
|
|
2080
|
+
'distinct', 'as', 'insert', 'into', 'values', 'update', 'set', 'delete',
|
|
2081
|
+
'create', 'table', 'view', 'function', 'procedure', 'index', 'alter', 'drop',
|
|
2082
|
+
'if', 'exists', 'begin', 'commit', 'rollback', 'return', 'declare', 'exec',
|
|
2083
|
+
'execute', 'call', 'with', 'cast', 'coalesce', 'nullif', 'extract', 'primary',
|
|
2084
|
+
'foreign', 'references', 'key', 'constraint', 'default', 'unique', 'check',
|
|
2085
|
+
'asc', 'desc', 'is', 'in', 'like', 'between', 'null', 'true', 'false',
|
|
2086
|
+
'interval', 'date', 'using', 'natural', 'returns', 'language', 'temporary',
|
|
2087
|
+
'varchar', 'char', 'nvarchar', 'nchar', 'int', 'integer', 'bigint', 'smallint',
|
|
2088
|
+
'tinyint', 'text', 'boolean', 'bool', 'datetime', 'timestamp', 'timestamptz',
|
|
2089
|
+
'decimal', 'numeric', 'float', 'double', 'real', 'json', 'jsonb', 'uuid',
|
|
2090
|
+
'serial', 'bigserial', 'blob', 'bytea', 'bit', 'money', 'xml', 'enum',
|
|
2091
|
+
'count', 'sum', 'avg', 'min', 'max', 'round', 'upper', 'lower', 'trim',
|
|
2092
|
+
'length', 'concat', 'substring', 'replace', 'abs', 'ceil', 'floor', 'now',
|
|
2093
|
+
'current_timestamp', 'current_date', 'year', 'month', 'day', 'hour', 'minute',
|
|
2094
|
+
'second', 'convert', 'try_cast', 'iif', 'stuff', 'format', 'getdate',
|
|
2095
|
+
]);
|
|
2096
|
+
|
|
2097
|
+
// SQL reference extraction: one calls-record per created object whose segment
|
|
2098
|
+
// (its statement through the next CREATE) lists the SQL objects it touches —
|
|
2099
|
+
// FROM/JOIN targets, EXEC targets, and `name(` function calls. This is the
|
|
2100
|
+
// dependency edge impact analysis uses to surface dependent views/functions.
|
|
2101
|
+
function extractSqlFunctionCalls(filePath, content) {
|
|
2102
|
+
const objects = findSqlObjects(content);
|
|
2103
|
+
if (objects.length === 0) {
|
|
2104
|
+
return [];
|
|
2105
|
+
}
|
|
2106
|
+
const stripped = content
|
|
2107
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
2108
|
+
.replace(/--[^\n]*/g, '');
|
|
2109
|
+
|
|
2110
|
+
const records = [];
|
|
2111
|
+
for (const [objectIndex, sqlObject] of objects.entries()) {
|
|
2112
|
+
const bodyEnd = objectIndex + 1 < objects.length ? objects[objectIndex + 1].matchIndex : content.length;
|
|
2113
|
+
const segment = stripped.slice(sqlObject.matchIndex, Math.min(bodyEnd, sqlObject.matchIndex + 20000));
|
|
2114
|
+
|
|
2115
|
+
const calls = [];
|
|
2116
|
+
const addCall = (rawName) => {
|
|
2117
|
+
if (!rawName) {
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
const bareName = rawName.split('.').pop().replace(/["`]/g, '');
|
|
2121
|
+
if (!bareName || bareName.toLowerCase() === sqlObject.name.toLowerCase()) {
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
if (SQL_CALL_KEYWORDS.has(bareName.toLowerCase())) {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
if (!calls.includes(bareName)) {
|
|
2128
|
+
calls.push(bareName);
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
|
|
2132
|
+
for (const match of segment.matchAll(/\b(?:FROM|JOIN)\s+["`]?([A-Za-z_][A-Za-z0-9_$]*(?:\s*\.\s*["`]?[A-Za-z_][A-Za-z0-9_$]*)?)/gi)) {
|
|
2133
|
+
addCall(match[1]);
|
|
2134
|
+
}
|
|
2135
|
+
for (const match of segment.matchAll(/\bEXEC(?:UTE)?\s+(?:@\w+\s*=\s*)?["`]?([A-Za-z_][A-Za-z0-9_$]*)/gi)) {
|
|
2136
|
+
addCall(match[1]);
|
|
2137
|
+
}
|
|
2138
|
+
for (const match of segment.matchAll(/\b([A-Za-z_][A-Za-z0-9_$]*)\s*\(/g)) {
|
|
2139
|
+
addCall(match[1]);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
records.push({
|
|
2143
|
+
filePath,
|
|
2144
|
+
symbol: sqlObject.name,
|
|
2145
|
+
exported: true,
|
|
2146
|
+
calls,
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
return records;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
1915
2152
|
function extractFunctionCalls(filePath, content) {
|
|
2153
|
+
if (filePath.endsWith('.sql')) {
|
|
2154
|
+
return extractSqlFunctionCalls(filePath, content);
|
|
2155
|
+
}
|
|
1916
2156
|
return extractFunctionBlocks(content).map((block) => {
|
|
1917
2157
|
const calls = [];
|
|
1918
2158
|
for (const match of block.body.matchAll(/(?:\b|\.)([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g)) {
|