kibi-cli 2.31.1 → 2.35.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/dist/cli-register-proof.d.ts.map +1 -1
- package/dist/cli-register-proof.js +19 -7
- package/dist/cli-register-reporting.d.ts.map +1 -1
- package/dist/cli-register-reporting.js +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -1
- package/dist/commands/check-generated.d.ts +5 -1
- package/dist/commands/check-generated.d.ts.map +1 -1
- package/dist/commands/check-generated.js +24 -35
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +14 -5
- package/dist/commands/discovery-table.d.ts.map +1 -1
- package/dist/commands/discovery-table.js +51 -4
- package/dist/commands/doctor.js +19 -4
- package/dist/commands/init-helpers.d.ts.map +1 -1
- package/dist/commands/init-helpers.js +8 -5
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +109 -5
- package/dist/commands/prove.d.ts +48 -1
- package/dist/commands/prove.d.ts.map +1 -1
- package/dist/commands/prove.js +293 -8
- package/dist/commands/report.d.ts +1 -0
- package/dist/commands/report.d.ts.map +1 -1
- package/dist/commands/report.js +70 -5
- package/dist/commands/signoff.d.ts.map +1 -1
- package/dist/commands/signoff.js +20 -1
- package/dist/commands/sync/manifest.d.ts +10 -1
- package/dist/commands/sync/manifest.d.ts.map +1 -1
- package/dist/commands/sync/manifest.js +106 -17
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +16 -5
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +19 -0
- package/dist/diagnostics.js +1 -1
- package/dist/engine.js +1 -1
- package/dist/extractors/markdown.d.ts +4 -2
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +17 -0
- package/dist/extractors/symbol-coordinates.js +2 -2
- package/dist/extractors/symbols-coordinator.d.ts +2 -4
- package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
- package/dist/extractors/symbols-coordinator.js +1 -2
- package/dist/operations/bootstrap/candidate-helpers.d.ts +3 -0
- package/dist/operations/bootstrap/candidate-helpers.d.ts.map +1 -1
- package/dist/operations/bootstrap/candidate-helpers.js +4 -0
- package/dist/operations/migration/evidence-floor.d.ts +41 -0
- package/dist/operations/migration/evidence-floor.d.ts.map +1 -0
- package/dist/operations/migration/evidence-floor.js +230 -0
- package/dist/operations/migration/proof-contract-claims.d.ts +72 -0
- package/dist/operations/migration/proof-contract-claims.d.ts.map +1 -0
- package/dist/operations/migration/proof-contract-claims.js +215 -0
- package/dist/operations/migration/schema6.d.ts +6 -0
- package/dist/operations/migration/schema6.d.ts.map +1 -1
- package/dist/operations/migration/schema6.js +92 -0
- package/dist/operations/migration/symbol-coordinates-refresh.d.ts +3 -2
- package/dist/operations/migration/symbol-coordinates-refresh.d.ts.map +1 -1
- package/dist/operations/migration/symbol-coordinates-refresh.js +4 -3
- package/dist/operations/migration/untrack-symbol-coordinates.d.ts +23 -0
- package/dist/operations/migration/untrack-symbol-coordinates.d.ts.map +1 -0
- package/dist/operations/migration/untrack-symbol-coordinates.js +67 -0
- package/dist/operations/modeling/requirement-modeler.d.ts.map +1 -1
- package/dist/operations/modeling/requirement-modeler.js +48 -1
- package/dist/operations/modeling/requirement-types.d.ts +2 -1
- package/dist/operations/modeling/requirement-types.d.ts.map +1 -1
- package/dist/operations/modeling/requirement-utils.d.ts +1 -1
- package/dist/operations/modeling/requirement-utils.d.ts.map +1 -1
- package/dist/operations/modeling/requirement-utils.js +8 -1
- package/dist/operations/mutation/omitted-properties.d.ts +11 -0
- package/dist/operations/mutation/omitted-properties.d.ts.map +1 -0
- package/dist/operations/mutation/omitted-properties.js +76 -0
- package/dist/operations/mutation/serialization.d.ts.map +1 -1
- package/dist/operations/mutation/serialization.js +1 -0
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +39 -20
- package/dist/operations/mutation/strict-fact.d.ts.map +1 -1
- package/dist/operations/mutation/strict-fact.js +6 -2
- package/dist/operations/mutation/symbol-granularity.d.ts.map +1 -1
- package/dist/operations/mutation/symbol-granularity.js +5 -3
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +7 -1
- package/dist/operations/mutation/validation.d.ts +2 -0
- package/dist/operations/mutation/validation.d.ts.map +1 -1
- package/dist/operations/mutation/validation.js +4 -0
- package/dist/operations/proof/claim-bindings.d.ts +41 -0
- package/dist/operations/proof/claim-bindings.d.ts.map +1 -0
- package/dist/operations/proof/claim-bindings.js +364 -0
- package/dist/operations/proof/code-certificates.d.ts +155 -0
- package/dist/operations/proof/code-certificates.d.ts.map +1 -0
- package/dist/operations/proof/code-certificates.js +894 -0
- package/dist/operations/proof/code-models.d.ts +96 -0
- package/dist/operations/proof/code-models.d.ts.map +1 -0
- package/dist/operations/proof/code-models.js +601 -0
- package/dist/operations/proof/code-runner.d.ts +40 -0
- package/dist/operations/proof/code-runner.d.ts.map +1 -0
- package/dist/operations/proof/code-runner.js +164 -0
- package/dist/operations/proof/code-scope.d.ts +14 -1
- package/dist/operations/proof/code-scope.d.ts.map +1 -1
- package/dist/operations/proof/code-scope.js +44 -2
- package/dist/operations/proof/compact-receipts.d.ts.map +1 -1
- package/dist/operations/proof/compact-receipts.js +4 -3
- package/dist/operations/proof/explain.d.ts +32 -0
- package/dist/operations/proof/explain.d.ts.map +1 -1
- package/dist/operations/proof/explain.js +91 -0
- package/dist/operations/proof/ingest-proof.d.ts +9 -1
- package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
- package/dist/operations/proof/ingest-proof.js +42 -2
- package/dist/operations/proof/receipt-compaction.d.ts +2 -0
- package/dist/operations/proof/receipt-compaction.d.ts.map +1 -1
- package/dist/operations/proof/receipt-compaction.js +6 -2
- package/dist/operations/proof/regular-language.d.ts +89 -0
- package/dist/operations/proof/regular-language.d.ts.map +1 -0
- package/dist/operations/proof/regular-language.js +943 -0
- package/dist/operations/proof/source-code-readers.d.ts +12 -0
- package/dist/operations/proof/source-code-readers.d.ts.map +1 -0
- package/dist/operations/proof/source-code-readers.js +699 -0
- package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analyze-prose.js +2 -1
- package/dist/operations/semantic-advisor/shared.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/shared.js +4 -0
- package/dist/operations/semantic-advisor/strict-count-rules.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/strict-count-rules.js +8 -2
- package/dist/operations/semantic-advisor/strict-rules.d.ts +8 -0
- package/dist/operations/semantic-advisor/strict-rules.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/strict-rules.js +234 -4
- package/dist/operations/semantic-advisor/types.d.ts +3 -2
- package/dist/operations/semantic-advisor/types.d.ts.map +1 -1
- package/dist/plugins/approved-source-analyzers.generated.d.ts +6 -6
- package/dist/plugins/approved-source-analyzers.generated.js +6 -6
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +13 -3
- package/dist/proof/command-test-report.d.ts +4 -3
- package/dist/proof/command-test-report.d.ts.map +1 -1
- package/dist/proof/command-test-report.js +4 -10
- package/dist/proof/evaluate.d.ts.map +1 -1
- package/dist/proof/evaluate.js +18 -2
- package/dist/proof/inspect.d.ts.map +1 -1
- package/dist/proof/inspect.js +58 -0
- package/dist/proof/producers/playwright-reporter.d.ts +3 -0
- package/dist/proof/producers/playwright-reporter.d.ts.map +1 -1
- package/dist/proof/producers/playwright-reporter.js +25 -2
- package/dist/proof/step-runner.d.ts +36 -0
- package/dist/proof/step-runner.d.ts.map +1 -0
- package/dist/proof/step-runner.js +234 -0
- package/dist/public/evidence-levels.d.ts +12 -0
- package/dist/public/evidence-levels.d.ts.map +1 -0
- package/dist/public/evidence-levels.js +19 -0
- package/dist/public/impact/coverage-depth-quality.d.ts +7 -0
- package/dist/public/impact/coverage-depth-quality.d.ts.map +1 -1
- package/dist/public/impact/coverage-depth-quality.js +59 -0
- package/dist/public/impact/full-kb-quality.d.ts.map +1 -1
- package/dist/public/impact/full-kb-quality.js +3 -1
- package/dist/public/operations/catalog.d.ts +315 -11
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/check-executor.d.ts +2 -0
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +6 -0
- package/dist/public/operations/evidence-floor.d.ts +48 -0
- package/dist/public/operations/evidence-floor.d.ts.map +1 -0
- package/dist/public/operations/evidence-floor.js +143 -0
- package/dist/public/operations/migration-plan.d.ts +19 -5
- package/dist/public/operations/migration-plan.d.ts.map +1 -1
- package/dist/public/operations/migration-plan.js +22 -5
- package/dist/public/operations/node-ports.d.ts.map +1 -1
- package/dist/public/operations/node-ports.js +4 -1
- package/dist/public/operations/proof-contract-claims.d.ts +26 -0
- package/dist/public/operations/proof-contract-claims.d.ts.map +1 -0
- package/dist/public/operations/proof-contract-claims.js +71 -0
- package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
- package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
- package/dist/public/operations/specs/check-rules.generated.js +3 -1
- package/dist/public/operations/specs/check.d.ts +1 -1
- package/dist/public/operations/specs/composite.d.ts +16 -3
- package/dist/public/operations/specs/composite.d.ts.map +1 -1
- package/dist/public/operations/specs/modeling.d.ts +16 -3
- package/dist/public/operations/specs/modeling.d.ts.map +1 -1
- package/dist/public/operations/specs/modeling.js +22 -3
- package/dist/public/operations/specs/mutation-schemas.d.ts +289 -7
- package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
- package/dist/public/operations/specs/mutation-schemas.js +28 -4
- package/dist/public/operations/specs/proof.d.ts +9 -0
- package/dist/public/operations/specs/proof.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.js +19 -10
- package/dist/public/operations/specs/upsert.d.ts +289 -7
- package/dist/public/operations/specs/upsert.d.ts.map +1 -1
- package/dist/public/proof-certificates.d.ts +163 -0
- package/dist/public/proof-certificates.d.ts.map +1 -0
- package/dist/public/proof-certificates.js +419 -0
- package/dist/public/proof-protocol.d.ts +259 -3
- package/dist/public/proof-protocol.d.ts.map +1 -1
- package/dist/public/proof-protocol.js +524 -3
- package/dist/public/proof-receipt.d.ts +90 -6
- package/dist/public/proof-receipt.d.ts.map +1 -1
- package/dist/public/proof-receipt.js +88 -7
- package/dist/public/proof-sources.d.ts +102 -0
- package/dist/public/proof-sources.d.ts.map +1 -0
- package/dist/public/proof-sources.js +1292 -0
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +39 -2
- package/dist/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/dist/public/skills/kibi-commit-review/SKILL.md +8 -8
- package/dist/public/skills/kibi-commit-review/resources/staged-gate.md +12 -7
- package/dist/public/skills/kibi-decisions/SKILL.md +13 -2
- package/dist/public/skills/kibi-decisions/resources/decision-records.md +9 -1
- package/dist/public/skills/kibi-freshness/SKILL.md +5 -1
- package/dist/public/skills/kibi-traceability/SKILL.md +12 -1
- package/dist/public/skills/kibi-usage/SKILL.md +8 -4
- package/dist/public/skills/kibi-usage/resources/proof.md +221 -20
- package/dist/public/skills/kibi-usage/resources/workflows.md +4 -4
- package/dist/report/html-report.css +83 -11
- package/dist/report/html-report.d.ts +8 -0
- package/dist/report/html-report.d.ts.map +1 -1
- package/dist/report/html-report.js +214 -22
- package/dist/report/report-view.d.ts +2 -2
- package/dist/report/report-view.d.ts.map +1 -1
- package/dist/report/report-view.js +8 -0
- package/dist/schemas/entity.schema.json +404 -5
- package/dist/search-answer.d.ts.map +1 -1
- package/dist/search-answer.js +1 -0
- package/dist/signoff/html.d.ts.map +1 -1
- package/dist/signoff/html.js +3 -0
- package/dist/signoff/markdown.d.ts.map +1 -1
- package/dist/signoff/markdown.js +3 -0
- package/dist/signoff/model.d.ts +20 -0
- package/dist/signoff/model.d.ts.map +1 -1
- package/dist/signoff/model.js +20 -1
- package/dist/traceability/evidence-model.d.ts +13 -12
- package/dist/traceability/evidence-model.d.ts.map +1 -1
- package/dist/traceability/evidence-model.js +1 -1
- package/dist/traceability/impact-knowledge.d.ts.map +1 -1
- package/dist/traceability/impact-knowledge.js +1 -0
- package/dist/traceability/staged-diagnostics.d.ts.map +1 -1
- package/dist/traceability/staged-diagnostics.js +24 -20
- package/dist/traceability/staged-impact-contract.d.ts +4 -4
- package/dist/traceability/staged-impact-contract.js +9 -9
- package/dist/traceability/staged-symbols-manifest.d.ts +11 -3
- package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
- package/dist/traceability/staged-symbols-manifest.js +47 -140
- package/dist/traceability/temp-kb.d.ts.map +1 -1
- package/dist/traceability/temp-kb.js +1 -0
- package/dist/types/entities.d.ts +5 -2
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/utils/merge-driver-setup.d.ts +8 -1
- package/dist/utils/merge-driver-setup.d.ts.map +1 -1
- package/dist/utils/merge-driver-setup.js +23 -2
- package/dist/utils/rule-registry.generated.d.ts +10 -0
- package/dist/utils/rule-registry.generated.d.ts.map +1 -1
- package/dist/utils/rule-registry.generated.js +12 -0
- package/dist/utils/schema-version.d.ts +1 -1
- package/dist/utils/schema-version.d.ts.map +1 -1
- package/dist/utils/schema-version.js +7 -1
- package/dist/utils/strict-modeling.d.ts +3 -2
- package/dist/utils/strict-modeling.d.ts.map +1 -1
- package/dist/utils/strict-modeling.js +37 -4
- package/dist/workspace-cwd.d.ts +15 -0
- package/dist/workspace-cwd.d.ts.map +1 -0
- package/dist/workspace-cwd.js +86 -0
- package/package.json +6 -2
- package/schema/entities.pl +6 -0
- package/schema/validation.pl +7 -2
- package/src/public/evidence-levels.ts +23 -0
- package/src/public/impact/coverage-depth-quality.ts +73 -0
- package/src/public/impact/full-kb-quality.ts +6 -1
- package/src/public/operations/check-executor.ts +13 -0
- package/src/public/operations/evidence-floor.ts +203 -0
- package/src/public/operations/migration-plan.ts +39 -6
- package/src/public/operations/node-ports.ts +3 -1
- package/src/public/operations/proof-contract-claims.ts +103 -0
- package/src/public/operations/specs/check-rules.generated.ts +3 -1
- package/src/public/operations/specs/modeling.ts +24 -5
- package/src/public/operations/specs/mutation-schemas.ts +30 -4
- package/src/public/operations/specs/reporting.ts +20 -10
- package/src/public/proof-certificates.ts +566 -0
- package/src/public/proof-protocol-types.d.ts +69 -1
- package/src/public/proof-protocol.ts +694 -3
- package/src/public/proof-receipt.ts +133 -7
- package/src/public/proof-sources.ts +1656 -0
- package/src/public/schemas/entity.ts +51 -3
- package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
- package/src/public/skills/kibi-commit-review/SKILL.md +8 -8
- package/src/public/skills/kibi-commit-review/resources/staged-gate.md +12 -7
- package/src/public/skills/kibi-decisions/SKILL.md +13 -2
- package/src/public/skills/kibi-decisions/resources/decision-records.md +9 -1
- package/src/public/skills/kibi-freshness/SKILL.md +5 -1
- package/src/public/skills/kibi-traceability/SKILL.md +12 -1
- package/src/public/skills/kibi-usage/SKILL.md +8 -4
- package/src/public/skills/kibi-usage/resources/proof.md +221 -20
- package/src/public/skills/kibi-usage/resources/workflows.md +4 -4
- package/src/schemas/entity.schema.json +404 -5
- package/dist/commands/proof-impact.d.ts +0 -7
- package/dist/commands/proof-impact.d.ts.map +0 -1
- package/dist/commands/proof-impact.js +0 -39
- package/dist/operations/proof/baseline-diff.d.ts +0 -41
- package/dist/operations/proof/baseline-diff.d.ts.map +0 -1
- package/dist/operations/proof/baseline-diff.js +0 -153
- package/dist/operations/proof/impact.d.ts +0 -51
- package/dist/operations/proof/impact.d.ts.map +0 -1
- package/dist/operations/proof/impact.js +0 -138
- package/dist/traceability/reviewed-coordinate-refresh.d.ts +0 -10
- package/dist/traceability/reviewed-coordinate-refresh.d.ts.map +0 -1
- package/dist/traceability/reviewed-coordinate-refresh.js +0 -49
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The readers of `source` specs that need code: the references reader
|
|
20
|
+
* (environment variables, imports, calls, string arguments, thrown errors
|
|
21
|
+
* and property values, read from the syntax tree of TypeScript, JavaScript
|
|
22
|
+
* and Python files), and the readers that load a module (drizzle schemas,
|
|
23
|
+
* Hono and Express route tables, zod and pydantic validators).
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from "node:fs";
|
|
26
|
+
import { createRequire } from "node:module";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { matchWorkspaceFiles, unresolvedReason, } from "../../public/proof-sources.js";
|
|
29
|
+
import { runCodeEvaluator, TS_RESOLVE_HOOKS } from "./code-runner.js";
|
|
30
|
+
const JS_FILE = /\.(?:[cm]?[jt]sx?)$/i;
|
|
31
|
+
const PY_FILE = /\.py$/i;
|
|
32
|
+
function globList(value) {
|
|
33
|
+
if (typeof value === "string")
|
|
34
|
+
return [value];
|
|
35
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* TypeScript's parser: the workspace's install first, then Kibi's own, then
|
|
39
|
+
* the copy ts-morph bundles (Kibi's built-in source analysis depends on it),
|
|
40
|
+
* so a JavaScript project needs no TypeScript install of its own.
|
|
41
|
+
*/
|
|
42
|
+
function loadTypeScript(workspaceRoot) {
|
|
43
|
+
const attempts = [
|
|
44
|
+
() => createRequire(path.join(workspaceRoot, "package.json"))("typescript"),
|
|
45
|
+
() => createRequire(import.meta.url)("typescript"),
|
|
46
|
+
() => createRequire(import.meta.url)("ts-morph").ts,
|
|
47
|
+
() => {
|
|
48
|
+
const builtin = createRequire(import.meta.url).resolve("kibi-plugin-builtin");
|
|
49
|
+
return createRequire(builtin)("ts-morph").ts;
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
for (const attempt of attempts) {
|
|
53
|
+
try {
|
|
54
|
+
const ts = attempt();
|
|
55
|
+
if (typeof ts?.createSourceFile === "function")
|
|
56
|
+
return ts;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// try the next install
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Variables Vite defines on import.meta.env itself.
|
|
65
|
+
const VITE_BUILTINS = new Set(["MODE", "BASE_URL", "PROD", "DEV", "SSR"]);
|
|
66
|
+
/** References one TypeScript or JavaScript file makes. */
|
|
67
|
+
export function extractScriptReferences(ts, file, text, spec) {
|
|
68
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
69
|
+
const members = [];
|
|
70
|
+
const unresolved = [];
|
|
71
|
+
const at = (node) => ({
|
|
72
|
+
file,
|
|
73
|
+
line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
|
|
74
|
+
text: node.getText(source).replace(/\s+/g, " ").slice(0, 80),
|
|
75
|
+
});
|
|
76
|
+
const literal = (node) => node !== undefined && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
77
|
+
? node.text
|
|
78
|
+
: null;
|
|
79
|
+
// The expression under parentheses, `!`, `as T`, `<T>x` and `satisfies T`:
|
|
80
|
+
// a cast does not change what is read, so `(import.meta as X).env` is
|
|
81
|
+
// import.meta.env.
|
|
82
|
+
const unwrapped = (node) => {
|
|
83
|
+
if (ts.isNonNullExpression(node) ||
|
|
84
|
+
ts.isParenthesizedExpression(node) ||
|
|
85
|
+
ts.isAsExpression(node) ||
|
|
86
|
+
ts.isTypeAssertionExpression(node) ||
|
|
87
|
+
ts.isSatisfiesExpression(node))
|
|
88
|
+
return unwrapped(node.expression);
|
|
89
|
+
return node;
|
|
90
|
+
};
|
|
91
|
+
// a.b.c for identifier and property chains, a["b"] for literal element access
|
|
92
|
+
const chain = (node) => {
|
|
93
|
+
if (ts.isIdentifier(node))
|
|
94
|
+
return node.text;
|
|
95
|
+
if (node.kind === ts.SyntaxKind.ThisKeyword)
|
|
96
|
+
return "this";
|
|
97
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
98
|
+
const left = chain(node.expression);
|
|
99
|
+
return left === null ? null : `${left}.${node.name.text}`;
|
|
100
|
+
}
|
|
101
|
+
if (ts.isElementAccessExpression(node)) {
|
|
102
|
+
const key = literal(node.argumentExpression);
|
|
103
|
+
const left = chain(node.expression);
|
|
104
|
+
return left === null || key === null ? null : `${left}.${key}`;
|
|
105
|
+
}
|
|
106
|
+
if (ts.isMetaProperty(node))
|
|
107
|
+
return `${ts.tokenToString(node.keywordToken)}.${node.name.text}`;
|
|
108
|
+
const inner = unwrapped(node);
|
|
109
|
+
return inner === node ? null : chain(inner);
|
|
110
|
+
};
|
|
111
|
+
// Whether the expression, under any casts, is the object of a property read.
|
|
112
|
+
const readsAProperty = (node) => {
|
|
113
|
+
let current = node;
|
|
114
|
+
let parent = node.parent;
|
|
115
|
+
while (parent !== undefined && unwrapped(parent) !== parent) {
|
|
116
|
+
current = parent;
|
|
117
|
+
parent = parent.parent;
|
|
118
|
+
}
|
|
119
|
+
return (parent !== undefined &&
|
|
120
|
+
(ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) &&
|
|
121
|
+
parent.expression === current);
|
|
122
|
+
};
|
|
123
|
+
const extract = spec.extract;
|
|
124
|
+
const callee = typeof spec.callee === "string" ? spec.callee : "";
|
|
125
|
+
const key = typeof spec.key === "string" ? spec.key : "";
|
|
126
|
+
const ENV_OBJECTS = new Set(["process.env", "import.meta.env", "Bun.env"]);
|
|
127
|
+
const catchNames = [];
|
|
128
|
+
const visit = (node) => {
|
|
129
|
+
if (ts.isCatchClause(node) && node.variableDeclaration && ts.isIdentifier(node.variableDeclaration.name)) {
|
|
130
|
+
catchNames.push(node.variableDeclaration.name.text);
|
|
131
|
+
ts.forEachChild(node, visit);
|
|
132
|
+
catchNames.pop();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
switch (extract) {
|
|
136
|
+
case "env": {
|
|
137
|
+
const name = ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node) ? chain(node.expression) : null;
|
|
138
|
+
if (name !== null && ENV_OBJECTS.has(name)) {
|
|
139
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
140
|
+
if (!(name === "import.meta.env" && VITE_BUILTINS.has(node.name.text)))
|
|
141
|
+
members.push(node.name.text);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const value = literal(node.argumentExpression);
|
|
145
|
+
if (value === null)
|
|
146
|
+
unresolved.push(at(node));
|
|
147
|
+
else
|
|
148
|
+
members.push(value);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (ts.isCallExpression(node) && chain(node.expression) === "Deno.env.get") {
|
|
153
|
+
const value = literal(node.arguments[0]);
|
|
154
|
+
if (value === null)
|
|
155
|
+
unresolved.push(at(node));
|
|
156
|
+
else
|
|
157
|
+
members.push(value);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (ts.isVariableDeclaration(node) && node.initializer && ts.isObjectBindingPattern(node.name)) {
|
|
161
|
+
const from = chain(node.initializer);
|
|
162
|
+
if (from !== null && ENV_OBJECTS.has(from)) {
|
|
163
|
+
for (const element of node.name.elements) {
|
|
164
|
+
const property = element.propertyName ?? element.name;
|
|
165
|
+
if (element.dotDotDotToken || !(ts.isIdentifier(property) || ts.isStringLiteral(property)))
|
|
166
|
+
unresolved.push(at(element));
|
|
167
|
+
else
|
|
168
|
+
members.push(property.text);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// The whole environment object handed elsewhere can be read under any name.
|
|
174
|
+
if ((ts.isPropertyAccessExpression(node) || ts.isMetaProperty(node)) && ENV_OBJECTS.has(chain(node) ?? "")) {
|
|
175
|
+
unresolved.push(at(node));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// So can import.meta itself, when it is handed on instead of read.
|
|
179
|
+
if (ts.isMetaProperty(node) && chain(node) === "import.meta" && !readsAProperty(node)) {
|
|
180
|
+
unresolved.push(at(node));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "imports": {
|
|
186
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier) {
|
|
187
|
+
const value = literal(node.moduleSpecifier);
|
|
188
|
+
if (value !== null)
|
|
189
|
+
members.push(value);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
|
193
|
+
const value = literal(node.moduleReference.expression);
|
|
194
|
+
if (value !== null)
|
|
195
|
+
members.push(value);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
if (ts.isCallExpression(node) &&
|
|
199
|
+
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
200
|
+
(ts.isIdentifier(node.expression) && node.expression.text === "require"))) {
|
|
201
|
+
const value = literal(node.arguments[0]);
|
|
202
|
+
if (value === null)
|
|
203
|
+
unresolved.push(at(node));
|
|
204
|
+
else
|
|
205
|
+
members.push(value);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "calls": {
|
|
210
|
+
if (ts.isCallExpression(node)) {
|
|
211
|
+
const target = node.expression;
|
|
212
|
+
const name = chain(target);
|
|
213
|
+
if (name !== null)
|
|
214
|
+
members.push(name);
|
|
215
|
+
else if (ts.isElementAccessExpression(target))
|
|
216
|
+
unresolved.push(at(target));
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case "string_args": {
|
|
221
|
+
if (ts.isCallExpression(node)) {
|
|
222
|
+
const name = chain(node.expression);
|
|
223
|
+
if (name !== null && (name === callee || (!callee.includes(".") && name.endsWith(`.${callee}`)))) {
|
|
224
|
+
const value = literal(node.arguments[0]);
|
|
225
|
+
if (value === null)
|
|
226
|
+
unresolved.push(at(node));
|
|
227
|
+
else
|
|
228
|
+
members.push(value);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case "thrown": {
|
|
234
|
+
if (ts.isThrowStatement(node) && node.expression) {
|
|
235
|
+
const thrown = unwrapped(node.expression);
|
|
236
|
+
// throw new ForbiddenError(...) names the class; throw
|
|
237
|
+
// createForbiddenError(...) names the factory.
|
|
238
|
+
const name = ts.isNewExpression(thrown) || ts.isCallExpression(thrown) ? chain(thrown.expression) : null;
|
|
239
|
+
if (name !== null)
|
|
240
|
+
members.push(name);
|
|
241
|
+
else if (!(ts.isIdentifier(thrown) && catchNames.includes(thrown.text)))
|
|
242
|
+
unresolved.push(at(node));
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case "property": {
|
|
247
|
+
if (ts.isPropertyAssignment(node)) {
|
|
248
|
+
const name = node.name;
|
|
249
|
+
if ((ts.isIdentifier(name) || ts.isStringLiteral(name)) && name.text === key) {
|
|
250
|
+
const value = literal(node.initializer);
|
|
251
|
+
if (value === null)
|
|
252
|
+
unresolved.push(at(node));
|
|
253
|
+
else
|
|
254
|
+
members.push(value);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (ts.isShorthandPropertyAssignment(node) && node.name.text === key)
|
|
258
|
+
unresolved.push(at(node));
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
ts.forEachChild(node, visit);
|
|
263
|
+
};
|
|
264
|
+
visit(source);
|
|
265
|
+
return { members, unresolved };
|
|
266
|
+
}
|
|
267
|
+
const PYTHON_REFERENCES = `
|
|
268
|
+
import ast, json, sys
|
|
269
|
+
plan = json.loads(sys.argv[-1])
|
|
270
|
+
extract, callee, key = plan["extract"], plan.get("callee") or "", plan.get("key") or ""
|
|
271
|
+
members, unresolved = [], []
|
|
272
|
+
def chain(node):
|
|
273
|
+
if isinstance(node, ast.Name):
|
|
274
|
+
return node.id
|
|
275
|
+
if isinstance(node, ast.Attribute):
|
|
276
|
+
left = chain(node.value)
|
|
277
|
+
return None if left is None else left + "." + node.attr
|
|
278
|
+
return None
|
|
279
|
+
def literal(node):
|
|
280
|
+
return node.value if isinstance(node, ast.Constant) and isinstance(node.value, str) else None
|
|
281
|
+
ENV_GETTERS = {"os.getenv", "getenv", "os.environ.get", "environ.get"}
|
|
282
|
+
ENV_OBJECTS = {"os.environ", "environ"}
|
|
283
|
+
for file in plan["files"]:
|
|
284
|
+
with open(file, encoding="utf-8") as handle:
|
|
285
|
+
text = handle.read()
|
|
286
|
+
try:
|
|
287
|
+
tree = ast.parse(text, filename=file)
|
|
288
|
+
except SyntaxError as error:
|
|
289
|
+
print(json.dumps({"ok": False, "error": file + ": " + str(error)}))
|
|
290
|
+
sys.exit(0)
|
|
291
|
+
lines = text.splitlines()
|
|
292
|
+
def miss(node):
|
|
293
|
+
line = getattr(node, "lineno", 1)
|
|
294
|
+
unresolved.append({"file": file, "line": line, "text": (lines[line - 1].strip() if line - 1 < len(lines) else "")[:80]})
|
|
295
|
+
caught = set()
|
|
296
|
+
for node in ast.walk(tree):
|
|
297
|
+
if isinstance(node, ast.ExceptHandler) and node.name:
|
|
298
|
+
caught.add(node.name)
|
|
299
|
+
for node in ast.walk(tree):
|
|
300
|
+
if extract == "env":
|
|
301
|
+
if isinstance(node, ast.Call) and chain(node.func) in ENV_GETTERS:
|
|
302
|
+
value = literal(node.args[0]) if node.args else None
|
|
303
|
+
(members.append(value) if value is not None else miss(node))
|
|
304
|
+
elif isinstance(node, ast.Subscript) and chain(node.value) in ENV_OBJECTS:
|
|
305
|
+
value = literal(node.slice)
|
|
306
|
+
(members.append(value) if value is not None else miss(node))
|
|
307
|
+
elif extract == "imports":
|
|
308
|
+
if isinstance(node, ast.Import):
|
|
309
|
+
members.extend(alias.name for alias in node.names)
|
|
310
|
+
elif isinstance(node, ast.ImportFrom):
|
|
311
|
+
members.append("." * node.level + (node.module or ""))
|
|
312
|
+
elif isinstance(node, ast.Call) and chain(node.func) in {"importlib.import_module", "import_module", "__import__"}:
|
|
313
|
+
value = literal(node.args[0]) if node.args else None
|
|
314
|
+
(members.append(value) if value is not None else miss(node))
|
|
315
|
+
elif extract == "calls":
|
|
316
|
+
if isinstance(node, ast.Call):
|
|
317
|
+
name = chain(node.func)
|
|
318
|
+
if name is not None:
|
|
319
|
+
members.append(name)
|
|
320
|
+
elif isinstance(node.func, ast.Subscript):
|
|
321
|
+
miss(node)
|
|
322
|
+
elif extract == "string_args":
|
|
323
|
+
if isinstance(node, ast.Call):
|
|
324
|
+
name = chain(node.func)
|
|
325
|
+
if name is not None and (name == callee or ("." not in callee and name.endswith("." + callee))):
|
|
326
|
+
value = literal(node.args[0]) if node.args else None
|
|
327
|
+
(members.append(value) if value is not None else miss(node))
|
|
328
|
+
elif extract == "thrown":
|
|
329
|
+
if isinstance(node, ast.Raise) and node.exc is not None:
|
|
330
|
+
target = node.exc.func if isinstance(node.exc, ast.Call) else node.exc
|
|
331
|
+
name = chain(target)
|
|
332
|
+
if name is not None and not (isinstance(node.exc, ast.Name) and name in caught):
|
|
333
|
+
members.append(name)
|
|
334
|
+
elif name is None:
|
|
335
|
+
miss(node)
|
|
336
|
+
elif extract == "property":
|
|
337
|
+
if isinstance(node, ast.Dict):
|
|
338
|
+
for k, v in zip(node.keys, node.values):
|
|
339
|
+
if k is not None and literal(k) == key:
|
|
340
|
+
value = literal(v)
|
|
341
|
+
(members.append(value) if value is not None else miss(v))
|
|
342
|
+
elif isinstance(node, ast.Call):
|
|
343
|
+
for keyword in node.keywords:
|
|
344
|
+
if keyword.arg == key:
|
|
345
|
+
value = literal(keyword.value)
|
|
346
|
+
(members.append(value) if value is not None else miss(keyword.value))
|
|
347
|
+
print(json.dumps({"ok": True, "members": members, "unresolved": unresolved}))
|
|
348
|
+
`;
|
|
349
|
+
const EXTRACT_WHAT = {
|
|
350
|
+
env: "environment variable reads",
|
|
351
|
+
imports: "imports",
|
|
352
|
+
calls: "calls",
|
|
353
|
+
string_args: "first arguments",
|
|
354
|
+
thrown: "throw statements",
|
|
355
|
+
property: "property values",
|
|
356
|
+
};
|
|
357
|
+
async function readReferences(workspaceRoot, spec) {
|
|
358
|
+
const files = matchWorkspaceFiles(workspaceRoot, globList(spec.files), globList(spec.ignore));
|
|
359
|
+
if (files.length === 0)
|
|
360
|
+
return { ok: false, reason: `no file matches ${globList(spec.files).join(", ")}` };
|
|
361
|
+
const other = files.filter((file) => !JS_FILE.test(file) && !PY_FILE.test(file));
|
|
362
|
+
if (other.length > 0)
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
reason: `the references reader reads TypeScript, JavaScript and Python files; narrow files to exclude ${other.slice(0, 3).join(", ")}`,
|
|
366
|
+
};
|
|
367
|
+
const members = [];
|
|
368
|
+
const unresolved = [];
|
|
369
|
+
const scripts = files.filter((file) => JS_FILE.test(file));
|
|
370
|
+
if (scripts.length > 0) {
|
|
371
|
+
const ts = loadTypeScript(workspaceRoot);
|
|
372
|
+
if (ts === null)
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
reason: "Kibi reads TypeScript and JavaScript references with the TypeScript parser; install typescript in the workspace",
|
|
376
|
+
};
|
|
377
|
+
for (const file of scripts) {
|
|
378
|
+
const found = extractScriptReferences(ts, file, readFileSync(path.join(workspaceRoot, file), "utf8"), spec);
|
|
379
|
+
members.push(...found.members);
|
|
380
|
+
unresolved.push(...found.unresolved);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const python = files.filter((file) => PY_FILE.test(file));
|
|
384
|
+
if (python.length > 0) {
|
|
385
|
+
const run = runCodeEvaluator(workspaceRoot, python[0], { js: "", python: PYTHON_REFERENCES }, [JSON.stringify({ extract: spec.extract, callee: spec.callee, key: spec.key, files: python })], (parsed) => parsed !== null && typeof parsed === "object" && "ok" in parsed);
|
|
386
|
+
if (!run.ok)
|
|
387
|
+
return { ok: false, reason: `Kibi could not run Python to read ${python[0]}${run.failure ? `: ${run.failure}` : ""}` };
|
|
388
|
+
const value = run.value;
|
|
389
|
+
if (!value.ok)
|
|
390
|
+
return { ok: false, reason: `Kibi could not parse ${value.error ?? "a Python file"}` };
|
|
391
|
+
members.push(...(value.members ?? []));
|
|
392
|
+
unresolved.push(...(value.unresolved ?? []));
|
|
393
|
+
}
|
|
394
|
+
if (unresolved.length > 0)
|
|
395
|
+
return { ok: false, reason: unresolvedReason(EXTRACT_WHAT[String(spec.extract)] ?? "references", unresolved) };
|
|
396
|
+
return { ok: true, members };
|
|
397
|
+
}
|
|
398
|
+
// Built on first use so TS_RESOLVE_HOOKS is initialized.
|
|
399
|
+
const jsModuleEvaluator = () => `
|
|
400
|
+
const plan = JSON.parse(process.argv.at(-1));
|
|
401
|
+
const { pathToFileURL } = await import("node:url");
|
|
402
|
+
const { createRequire } = await import("node:module");
|
|
403
|
+
if (typeof globalThis.Bun === "undefined") {
|
|
404
|
+
const { register } = await import("node:module");
|
|
405
|
+
register?.("data:text/javascript," + encodeURIComponent(${JSON.stringify(TS_RESOLVE_HOOKS)}));
|
|
406
|
+
}
|
|
407
|
+
const message = (error) => (error instanceof Error ? error.message : String(error));
|
|
408
|
+
const requireFrom = createRequire(plan.file);
|
|
409
|
+
// Express keeps no mount path on a router layer, so record it as routers are mounted.
|
|
410
|
+
const mounts = new WeakMap();
|
|
411
|
+
if (plan.reader === "routes") {
|
|
412
|
+
try {
|
|
413
|
+
const express = requireFrom("express");
|
|
414
|
+
let proto = Object.getPrototypeOf(express.Router());
|
|
415
|
+
while (proto && !Object.prototype.hasOwnProperty.call(proto, "use")) proto = Object.getPrototypeOf(proto);
|
|
416
|
+
const use = proto.use;
|
|
417
|
+
proto.use = function (...args) {
|
|
418
|
+
const before = this.stack.length;
|
|
419
|
+
const result = use.apply(this, args);
|
|
420
|
+
const mount = typeof args[0] === "function" || Array.isArray(args[0]) && typeof args[0][0] === "function" ? "/" : args[0];
|
|
421
|
+
for (const layer of this.stack.slice(before)) mounts.set(layer, mount);
|
|
422
|
+
return result;
|
|
423
|
+
};
|
|
424
|
+
} catch {}
|
|
425
|
+
}
|
|
426
|
+
async function exportsOf(file) {
|
|
427
|
+
return await import(pathToFileURL(file).href);
|
|
428
|
+
}
|
|
429
|
+
// Middleware by identity, and middleware a factory export makes: a factory
|
|
430
|
+
// such as requireAuth() returns a fresh closure per route, so its result is
|
|
431
|
+
// known by its source text and labelled with the factory name. A text two
|
|
432
|
+
// factories share names neither.
|
|
433
|
+
async function names() {
|
|
434
|
+
const byValue = new Map();
|
|
435
|
+
const byText = new Map();
|
|
436
|
+
for (const file of plan.names_from ?? []) {
|
|
437
|
+
const mod = await exportsOf(file);
|
|
438
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
439
|
+
if (typeof value !== "function") continue;
|
|
440
|
+
if (!byValue.has(value)) byValue.set(value, name);
|
|
441
|
+
// Only plain zero-argument functions: never a class, an async function
|
|
442
|
+
// or a generator, whose call could do work.
|
|
443
|
+
if (value.length !== 0 || value.constructor?.name !== "Function" || /^class\b/.test(Function.prototype.toString.call(value))) continue;
|
|
444
|
+
let made;
|
|
445
|
+
try {
|
|
446
|
+
made = value();
|
|
447
|
+
} catch {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (made && typeof made.then === "function") {
|
|
451
|
+
made.then(() => {}, () => {});
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (typeof made !== "function") continue;
|
|
455
|
+
const text = Function.prototype.toString.call(made);
|
|
456
|
+
byText.set(text, byText.has(text) && byText.get(text) !== name ? null : name);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
get(handler) {
|
|
461
|
+
if (byValue.has(handler)) return byValue.get(handler);
|
|
462
|
+
if (typeof handler !== "function") return undefined;
|
|
463
|
+
return byText.get(Function.prototype.toString.call(handler)) ?? undefined;
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const symbolNamed = (object, pattern) => Object.getOwnPropertySymbols(object).find((symbol) => pattern.test(String(symbol.description)));
|
|
468
|
+
async function drizzle(mod) {
|
|
469
|
+
const isTable = Symbol.for("drizzle:IsDrizzleTable");
|
|
470
|
+
const entities = [];
|
|
471
|
+
const chosen = plan.exports ?? Object.keys(mod);
|
|
472
|
+
for (const name of chosen) {
|
|
473
|
+
const table = mod[name];
|
|
474
|
+
if (table === undefined) throw new Error(name + " is not exported from " + plan.file);
|
|
475
|
+
if (table === null || typeof table !== "object" || table[isTable] !== true) {
|
|
476
|
+
if (plan.exports) throw new Error(name + " is not a drizzle table");
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const tableName = table[Symbol.for("drizzle:Name")];
|
|
480
|
+
const columns = Object.values(table[Symbol.for("drizzle:Columns")] ?? {}).map((column) => ({
|
|
481
|
+
name: column.name,
|
|
482
|
+
nullable: column.notNull !== true,
|
|
483
|
+
optional: false,
|
|
484
|
+
}));
|
|
485
|
+
const foreign_keys = [];
|
|
486
|
+
const inline = symbolNamed(table, /^drizzle:(SQLite|Pg|MySql|SingleStore|Gel)InlineForeignKeys$/);
|
|
487
|
+
const dialect = inline ? { SQLite: "sqlite", Pg: "pg", MySql: "mysql", SingleStore: "singlestore", Gel: "gel" }[String(inline.description).match(/^drizzle:(\\w+?)InlineForeignKeys$/)[1]] : null;
|
|
488
|
+
if (dialect === null) throw new Error("Kibi does not know the dialect of drizzle table " + tableName);
|
|
489
|
+
let core;
|
|
490
|
+
try {
|
|
491
|
+
core = await import(pathToFileURL(requireFrom.resolve("drizzle-orm/" + dialect + "-core")).href);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
throw new Error("Kibi could not load drizzle-orm/" + dialect + "-core next to " + plan.file + ": " + message(error));
|
|
494
|
+
}
|
|
495
|
+
const config = core.getTableConfig(table);
|
|
496
|
+
for (const fk of config.foreignKeys) {
|
|
497
|
+
const ref = fk.reference();
|
|
498
|
+
const target = ref.foreignTable[Symbol.for("drizzle:Name")];
|
|
499
|
+
foreign_keys.push(tableName + "." + ref.columns.map((c) => c.name).join(",") + "->" + target + "." + ref.foreignColumns.map((c) => c.name).join(",") + (fk.onDelete ? " on delete " + String(fk.onDelete).toLowerCase() : ""));
|
|
500
|
+
}
|
|
501
|
+
for (const column of config.columns ?? []) {
|
|
502
|
+
const row = columns.find((c) => c.name === column.name);
|
|
503
|
+
if (row && column.notNull === true) row.nullable = false;
|
|
504
|
+
}
|
|
505
|
+
entities.push({ name: tableName, columns, foreign_keys });
|
|
506
|
+
}
|
|
507
|
+
if (entities.length === 0) throw new Error(plan.file + " exports no drizzle table");
|
|
508
|
+
return { entities };
|
|
509
|
+
}
|
|
510
|
+
function honoRoutes(app, named) {
|
|
511
|
+
const label = (handler) => named.get(handler) ?? (handler.name || "(anonymous)");
|
|
512
|
+
const pattern = (text) => {
|
|
513
|
+
if (text === "*" || text === "/*") return /^.*$/;
|
|
514
|
+
const body = text.replace(/\\/\\*$/, "").replace(/[.+^$()|[\\]\\\\{}]/g, "\\\\$&").replace(/\\*/g, ".*").replace(/\\/:[^/]+/g, "/[^/]+");
|
|
515
|
+
return new RegExp("^" + body + (text.endsWith("/*") ? "(?:/.*)?" : "") + "$");
|
|
516
|
+
};
|
|
517
|
+
const entries = app.routes;
|
|
518
|
+
const routes = [];
|
|
519
|
+
const seen = new Set();
|
|
520
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
521
|
+
const entry = entries[index];
|
|
522
|
+
if (entry.method === "ALL") continue;
|
|
523
|
+
const id = entry.method + " " + entry.path;
|
|
524
|
+
if (seen.has(id)) continue;
|
|
525
|
+
seen.add(id);
|
|
526
|
+
const middleware = [];
|
|
527
|
+
for (let before = 0; before < index; before++) {
|
|
528
|
+
const other = entries[before];
|
|
529
|
+
if ((other.method === "ALL" || other.method === entry.method) && pattern(other.path).test(entry.path)) middleware.push(label(other.handler));
|
|
530
|
+
}
|
|
531
|
+
routes.push({ method: entry.method.toUpperCase(), path: entry.path, middleware });
|
|
532
|
+
}
|
|
533
|
+
return routes;
|
|
534
|
+
}
|
|
535
|
+
function expressRoutes(app, named) {
|
|
536
|
+
const label = (layer) => named.get(layer.handle) ?? (layer.name && layer.name !== "<anonymous>" ? layer.name : "(anonymous)");
|
|
537
|
+
const stackOf = (handle) => (handle?._router ?? handle?.router ?? handle)?.stack;
|
|
538
|
+
const mountOf = (layer) => {
|
|
539
|
+
if (mounts.has(layer)) {
|
|
540
|
+
const mount = mounts.get(layer);
|
|
541
|
+
if (typeof mount !== "string") throw new Error("a router is mounted on a pattern Kibi cannot read: " + String(mount));
|
|
542
|
+
return mount;
|
|
543
|
+
}
|
|
544
|
+
const source = layer.regexp?.source;
|
|
545
|
+
if (layer.regexp?.fast_slash) return "/";
|
|
546
|
+
const match = source && source.match(/^\\^((?:\\\\\\/[\\w.~-]+)*)\\\\\\/\\?\\(\\?=\\\\\\/\\|\\$\\)$/);
|
|
547
|
+
if (!match) throw new Error("Kibi cannot read where a router is mounted" + (source ? " (" + source + ")" : ""));
|
|
548
|
+
return match[1].replace(/\\\\\\//g, "/") || "/";
|
|
549
|
+
};
|
|
550
|
+
const join = (mount, sub) => (mount === "/" ? sub : sub === "/" ? mount : mount.replace(/\\/$/, "") + sub);
|
|
551
|
+
const collect = (stack) => {
|
|
552
|
+
const out = [];
|
|
553
|
+
const earlier = [];
|
|
554
|
+
for (const layer of stack) {
|
|
555
|
+
if (layer.name === "query" || layer.name === "expressInit") continue;
|
|
556
|
+
if (layer.route) {
|
|
557
|
+
if (typeof layer.route.path !== "string") throw new Error("a route path is not a string: " + String(layer.route.path));
|
|
558
|
+
const methods = Object.keys(layer.route.methods).filter((m) => layer.route.methods[m]);
|
|
559
|
+
if (methods.includes("_all")) { earlier.push(layer); continue; }
|
|
560
|
+
const own = layer.route.stack.slice(0, -1).map(label);
|
|
561
|
+
for (const method of methods)
|
|
562
|
+
out.push({ method: method.toUpperCase(), path: layer.route.path, middleware: [...earlier.filter((e) => e.match(layer.route.path)).map(label), ...own] });
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const nested = stackOf(layer.handle);
|
|
566
|
+
if (Array.isArray(nested)) {
|
|
567
|
+
const mount = mountOf(layer);
|
|
568
|
+
for (const route of collect(nested)) {
|
|
569
|
+
const full = join(mount, route.path);
|
|
570
|
+
out.push({ method: route.method, path: full, middleware: [...earlier.filter((e) => e.match(full)).map(label), ...route.middleware] });
|
|
571
|
+
}
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
earlier.push(layer);
|
|
575
|
+
}
|
|
576
|
+
return out;
|
|
577
|
+
};
|
|
578
|
+
const stack = stackOf(app);
|
|
579
|
+
if (!Array.isArray(stack)) throw new Error("the express app has no router stack");
|
|
580
|
+
return collect(stack);
|
|
581
|
+
}
|
|
582
|
+
async function routes(mod) {
|
|
583
|
+
let app = mod[plan.export];
|
|
584
|
+
if (app === undefined) throw new Error(plan.export + " is not exported from " + plan.file);
|
|
585
|
+
const named = await names();
|
|
586
|
+
const isExpress = (value) => typeof value === "function" && typeof value.use === "function" && typeof value.handle === "function";
|
|
587
|
+
if (typeof app === "function" && !isExpress(app)) app = await app();
|
|
588
|
+
const framework = plan.framework ?? (Array.isArray(app?.routes) ? "hono" : isExpress(app) || Array.isArray(app?.stack) ? "express" : null);
|
|
589
|
+
if (framework === "hono") return { routes: honoRoutes(app, named) };
|
|
590
|
+
if (framework === "express") return { routes: expressRoutes(app, named) };
|
|
591
|
+
throw new Error(plan.export + " is neither a Hono nor an Express app; name the framework");
|
|
592
|
+
}
|
|
593
|
+
// The fields of an object schema: zod's shape (under refine, transform,
|
|
594
|
+
// pipe or effects), or valibot's entries (under pipe).
|
|
595
|
+
function shapeOf(schema) {
|
|
596
|
+
let current = schema;
|
|
597
|
+
for (let depth = 0; depth < 10 && current; depth++) {
|
|
598
|
+
const shape = typeof current.shape === "function" ? current.shape() : current.shape;
|
|
599
|
+
if (shape && typeof shape === "object") return shape;
|
|
600
|
+
if (current.kind === "schema" && current.entries && typeof current.entries === "object") return current.entries;
|
|
601
|
+
const def = current._def ?? current._zod?.def;
|
|
602
|
+
current = def?.schema ?? def?.innerType ?? def?.in ?? (Array.isArray(current.pipe) && current.pipe[0] !== current ? current.pipe[0] : undefined);
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
// Whether a field accepts a value: zod's safeParse, or any Standard Schema
|
|
607
|
+
// (valibot, arktype, zod 3.24+).
|
|
608
|
+
async function accepts(field, value) {
|
|
609
|
+
if (typeof field?.safeParse === "function") return field.safeParse(value).success === true;
|
|
610
|
+
const standard = field?.["~standard"];
|
|
611
|
+
if (standard && typeof standard.validate === "function") {
|
|
612
|
+
const result = await standard.validate(value);
|
|
613
|
+
return !result.issues;
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
async function validator(mod) {
|
|
618
|
+
const schema = mod[plan.export];
|
|
619
|
+
if (schema === undefined) throw new Error(plan.export + " is not exported from " + plan.file);
|
|
620
|
+
const shape = shapeOf(schema);
|
|
621
|
+
if (shape === null) throw new Error(plan.export + " is not an object schema whose fields Kibi can list (a zod or valibot object, or one wrapped in refine, transform or pipe)");
|
|
622
|
+
const columns = [];
|
|
623
|
+
for (const [name, field] of Object.entries(shape)) {
|
|
624
|
+
const optional = await accepts(field, undefined);
|
|
625
|
+
const nullable = await accepts(field, null);
|
|
626
|
+
if (optional === null || nullable === null) throw new Error("field " + name + " of " + plan.export + " is neither a zod schema nor a Standard Schema");
|
|
627
|
+
columns.push({ name, optional, nullable });
|
|
628
|
+
}
|
|
629
|
+
return { entities: [{ name: plan.export, columns, foreign_keys: [] }] };
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
const mod = await exportsOf(plan.file);
|
|
633
|
+
const result = plan.reader === "routes" ? await routes(mod) : plan.reader === "validator" ? await validator(mod) : await drizzle(mod);
|
|
634
|
+
console.log(JSON.stringify({ ok: true, ...result }));
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.log(JSON.stringify({ ok: false, error: message(error) }));
|
|
637
|
+
}
|
|
638
|
+
`;
|
|
639
|
+
const PYTHON_MODULE_EVALUATOR = `
|
|
640
|
+
import importlib.util, json, sys, types, typing
|
|
641
|
+
plan = json.loads(sys.argv[-1])
|
|
642
|
+
def load(file):
|
|
643
|
+
spec = importlib.util.spec_from_file_location("kibi_source_module", file)
|
|
644
|
+
module = importlib.util.module_from_spec(spec)
|
|
645
|
+
sys.path.insert(0, plan["root"])
|
|
646
|
+
spec.loader.exec_module(module)
|
|
647
|
+
return module
|
|
648
|
+
try:
|
|
649
|
+
if plan["reader"] != "validator":
|
|
650
|
+
raise ValueError("reader " + plan["reader"] + " reads TypeScript and JavaScript modules only")
|
|
651
|
+
model = getattr(load(plan["file"]), plan["export"], None)
|
|
652
|
+
if model is None:
|
|
653
|
+
raise ValueError(plan["export"] + " is not defined in " + plan["file"])
|
|
654
|
+
fields = getattr(model, "model_fields", None)
|
|
655
|
+
if not isinstance(fields, dict):
|
|
656
|
+
raise ValueError(plan["export"] + " is not a pydantic model (it has no model_fields)")
|
|
657
|
+
columns = []
|
|
658
|
+
for name, field in fields.items():
|
|
659
|
+
annotation = field.annotation
|
|
660
|
+
args = typing.get_args(annotation)
|
|
661
|
+
nullable = annotation is type(None) or type(None) in args
|
|
662
|
+
columns.append({"name": field.alias or name, "optional": not field.is_required(), "nullable": nullable})
|
|
663
|
+
print(json.dumps({"ok": True, "entities": [{"name": plan["export"], "columns": columns, "foreign_keys": []}]}))
|
|
664
|
+
except Exception as error:
|
|
665
|
+
print(json.dumps({"ok": False, "error": type(error).__name__ + ": " + str(error)}))
|
|
666
|
+
`;
|
|
667
|
+
function evaluateModule(workspaceRoot, spec) {
|
|
668
|
+
const file = String(spec.file);
|
|
669
|
+
const absolute = path.resolve(workspaceRoot, file);
|
|
670
|
+
const plan = {
|
|
671
|
+
reader: spec.reader,
|
|
672
|
+
root: path.resolve(workspaceRoot),
|
|
673
|
+
file: absolute,
|
|
674
|
+
export: spec.export,
|
|
675
|
+
exports: spec.exports,
|
|
676
|
+
framework: spec.framework,
|
|
677
|
+
names_from: globList(spec.names_from).map((name) => path.resolve(workspaceRoot, name)),
|
|
678
|
+
};
|
|
679
|
+
const run = runCodeEvaluator(workspaceRoot, file, { js: jsModuleEvaluator(), python: PYTHON_MODULE_EVALUATOR }, [JSON.stringify(plan)], (parsed) => parsed !== null && typeof parsed === "object" && "ok" in parsed);
|
|
680
|
+
if (!run.ok)
|
|
681
|
+
return { ok: false, reason: `Kibi could not load ${file}${run.failure ? `: ${run.failure}` : ""}` };
|
|
682
|
+
const value = run.value;
|
|
683
|
+
if (!value.ok)
|
|
684
|
+
return { ok: false, reason: `Kibi could not read ${file}: ${value.error ?? "unknown error"}` };
|
|
685
|
+
return {
|
|
686
|
+
ok: true,
|
|
687
|
+
...(value.entities ? { entities: value.entities } : {}),
|
|
688
|
+
...(value.routes ? { routes: value.routes } : {}),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
/** The CLI's evaluator for source readers that need code. */
|
|
692
|
+
// implements REQ-kibi-source-proof-kinds
|
|
693
|
+
export const evaluateSourceCode = async (workspaceRoot, spec) => {
|
|
694
|
+
if (spec.reader === "references")
|
|
695
|
+
return readReferences(workspaceRoot, spec);
|
|
696
|
+
if (spec.reader === "schema" && spec.format !== "drizzle")
|
|
697
|
+
return { ok: false, reason: `format ${String(spec.format)} is read without code` };
|
|
698
|
+
return evaluateModule(workspaceRoot, spec);
|
|
699
|
+
};
|