@massu/core 1.10.8 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1334 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +581 -448
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +67 -19
- package/dist/hooks/pre-compact.js +28 -3
- package/dist/hooks/pre-delete-check.js +53 -27
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +31 -6
- package/dist/hooks/session-start.js +8709 -8459
- package/dist/hooks/user-prompt.js +25 -1
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Shared passthrough helpers for config migration + refresh.
|
|
6
|
-
*
|
|
7
|
-
* These helpers exist to prevent the class of bug fixed in @massu/core@1.2.0
|
|
8
|
-
* (incident 2026-04-19-config-upgrade-data-loss): a hand-maintained allow-list
|
|
9
|
-
* in `migrate.ts` silently dropped any v1 top-level key not on the list, and
|
|
10
|
-
* the parallel rebuild blocks inside `framework` / `paths` / `project` /
|
|
11
|
-
* `python` did the same thing at the nested level.
|
|
12
|
-
*
|
|
13
|
-
* Both helpers are TARGET-WINS: the migrator writes the keys it actively owns,
|
|
14
|
-
* then the helper fills in everything else the user had. A user-authored value
|
|
15
|
-
* in `target` is NEVER overwritten by the source.
|
|
16
|
-
*
|
|
17
|
-
* Why two exports instead of one:
|
|
18
|
-
* - `copyUnknownKeys` takes an explicit `handledKeys` set — used for TOP-LEVEL
|
|
19
|
-
* passthrough where the caller enumerates the keys it migrated explicitly
|
|
20
|
-
* (e.g., schema_version, project, framework, paths, toolPrefix, …).
|
|
21
|
-
* - `preserveNestedSubkeys` takes no handled-set — used for NESTED passthrough
|
|
22
|
-
* where the target block was just rebuilt, so `k in target` already skips
|
|
23
|
-
* any key the rebuild populated. Splitting the two makes callsites
|
|
24
|
-
* self-documenting without a verbose `new Set()` argument at every nested
|
|
25
|
-
* call (A-002 architecture-review follow-up).
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/** Keys that would mutate Object.prototype if copied as own properties. Explicit
|
|
29
|
-
* denylist defense-in-depth on top of the existing `k in target` guard and the
|
|
30
|
-
* `yaml@2.8` parser's non-polluting behavior (S-001 security-review follow-up). */
|
|
31
|
-
const UNSAFE_KEYS: ReadonlySet<string> = new Set(['__proto__', 'constructor', 'prototype']);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Copy any key from `source` into `target` that target doesn't already have set,
|
|
35
|
-
* skipping keys listed in `handledKeys`. Target values ALWAYS win — this function
|
|
36
|
-
* never overwrites an existing target key.
|
|
37
|
-
*
|
|
38
|
-
* - If source[k] is undefined → skip (undefined is not a preservable value).
|
|
39
|
-
* - If k is an UNSAFE_KEYS entry → skip (prototype-pollution defense).
|
|
40
|
-
* - If handledKeys.has(k) → skip (caller has its own handling).
|
|
41
|
-
* - If target already owns k → skip (target wins).
|
|
42
|
-
* - Otherwise → target[k] = deepClone(source[k]).
|
|
43
|
-
*
|
|
44
|
-
* Values are DEEP-CLONED via structuredClone so that mutating the output v2
|
|
45
|
-
* object never reaches back into the v1 input (S-002 security-review follow-up).
|
|
46
|
-
* Preserves the migrator's "pure data in, pure data out" contract.
|
|
47
|
-
*/
|
|
48
|
-
export function copyUnknownKeys(
|
|
49
|
-
source: Record<string, unknown>,
|
|
50
|
-
target: Record<string, unknown>,
|
|
51
|
-
handledKeys: ReadonlySet<string>
|
|
52
|
-
): void {
|
|
53
|
-
if (source === null || typeof source !== 'object' || Array.isArray(source)) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
for (const k of Object.keys(source)) {
|
|
57
|
-
if (UNSAFE_KEYS.has(k)) continue;
|
|
58
|
-
if (source[k] === undefined) continue;
|
|
59
|
-
if (handledKeys.has(k)) continue;
|
|
60
|
-
if (Object.prototype.hasOwnProperty.call(target, k)) continue;
|
|
61
|
-
target[k] = safeClone(source[k]);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Preserve every subkey from sourceBlock into targetBlock that targetBlock
|
|
67
|
-
* doesn't already have. Target values ALWAYS win.
|
|
68
|
-
*
|
|
69
|
-
* If sourceBlock is not a plain object (string, array, null, undefined),
|
|
70
|
-
* return early — there are no subkeys to preserve. This matches the coercion
|
|
71
|
-
* semantics of `getRecord` in migrate.ts and prevents throws on loose v1 inputs
|
|
72
|
-
* like `framework: "typescript"`.
|
|
73
|
-
*
|
|
74
|
-
* Values are deep-cloned (see copyUnknownKeys); UNSAFE_KEYS are skipped.
|
|
75
|
-
*/
|
|
76
|
-
export function preserveNestedSubkeys(
|
|
77
|
-
sourceBlock: unknown,
|
|
78
|
-
targetBlock: Record<string, unknown>
|
|
79
|
-
): void {
|
|
80
|
-
if (
|
|
81
|
-
sourceBlock === null ||
|
|
82
|
-
sourceBlock === undefined ||
|
|
83
|
-
typeof sourceBlock !== 'object' ||
|
|
84
|
-
Array.isArray(sourceBlock)
|
|
85
|
-
) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const src = sourceBlock as Record<string, unknown>;
|
|
89
|
-
for (const k of Object.keys(src)) {
|
|
90
|
-
if (UNSAFE_KEYS.has(k)) continue;
|
|
91
|
-
if (src[k] === undefined) continue;
|
|
92
|
-
if (Object.prototype.hasOwnProperty.call(targetBlock, k)) continue;
|
|
93
|
-
targetBlock[k] = safeClone(src[k]);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** structuredClone with a fallback for environments without it (Node <17). */
|
|
98
|
-
function safeClone<T>(v: T): T {
|
|
99
|
-
if (typeof structuredClone === 'function') {
|
|
100
|
-
try {
|
|
101
|
-
return structuredClone(v);
|
|
102
|
-
} catch {
|
|
103
|
-
// structuredClone throws on functions, DOM nodes, etc. — YAML never produces
|
|
104
|
-
// those, but if a caller passes something exotic, fall through to shallow.
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return v;
|
|
108
|
-
}
|
|
@@ -1,449 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Plan 3b — Phase 1: Regex Fallback Introspector.
|
|
6
|
-
*
|
|
7
|
-
* Verbatim move of the per-language regex helpers that were previously in
|
|
8
|
-
* `codebase-introspector.ts` (Plan #2 P3-001). NO regex logic changes — only
|
|
9
|
-
* the import path. This preserves Plan #2's 13 vitest cases as-is (re-imports
|
|
10
|
-
* are updated in `codebase-introspector.test.ts` only if needed; the moved
|
|
11
|
-
* functions retain their original signatures).
|
|
12
|
-
*
|
|
13
|
-
* The new 2-tier introspector (`codebase-introspector.ts`) calls these
|
|
14
|
-
* functions ONLY for fields where AST adapters returned 'none' confidence.
|
|
15
|
-
* AST adapters produce values into separate `detected.<adapter.id>` blocks;
|
|
16
|
-
* regex output continues to go into `detected.python` / `.swift` /
|
|
17
|
-
* `.typescript` blocks.
|
|
18
|
-
*
|
|
19
|
-
* Design rules carried over from Plan #2:
|
|
20
|
-
* - File size cap: 256KB per file (skip silently if larger).
|
|
21
|
-
* - Sample cap: at most 3 files per adapter (sorted, deterministic order).
|
|
22
|
-
* - Total wall-clock budget: <2s on a 10K-file repo.
|
|
23
|
-
* - Filesystem-only. No network, no child processes, no DB.
|
|
24
|
-
* - Returns `null` for any field it can't confidently extract.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
28
|
-
import { resolve, join, basename } from 'path';
|
|
29
|
-
import type { DetectionResult } from './index.ts';
|
|
30
|
-
|
|
31
|
-
export const MAX_FILE_BYTES = 256 * 1024;
|
|
32
|
-
export const MAX_SAMPLES_PER_ADAPTER = 3;
|
|
33
|
-
export const MAX_DIR_DEPTH = 6;
|
|
34
|
-
|
|
35
|
-
// ============================================================
|
|
36
|
-
// Public types (re-exported from codebase-introspector for compat)
|
|
37
|
-
// ============================================================
|
|
38
|
-
|
|
39
|
-
export interface DetectedPython {
|
|
40
|
-
auth_dep?: string;
|
|
41
|
-
api_prefix_base?: string;
|
|
42
|
-
test_async_pattern?: string;
|
|
43
|
-
_provenance?: Record<string, string>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface DetectedSwift {
|
|
47
|
-
api_client_class?: string;
|
|
48
|
-
biometric_policy?: string;
|
|
49
|
-
_provenance?: Record<string, string>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface DetectedTypeScript {
|
|
53
|
-
trpc_router_builder?: string;
|
|
54
|
-
procedure_pattern?: string;
|
|
55
|
-
_provenance?: Record<string, string>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ============================================================
|
|
59
|
-
// Python adapter (FastAPI + Django)
|
|
60
|
-
// ============================================================
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Introspect Python sources. Probes both FastAPI router files (`routers/*.py`,
|
|
64
|
-
* `api/*.py`) and Django views (`views.py`). Returns the most-extracted shape.
|
|
65
|
-
*/
|
|
66
|
-
export function introspectPython(
|
|
67
|
-
detection: DetectionResult,
|
|
68
|
-
projectRoot: string,
|
|
69
|
-
): DetectedPython | null {
|
|
70
|
-
const sourceDir = resolveSourceDir(detection, 'python', projectRoot);
|
|
71
|
-
if (!sourceDir) return null;
|
|
72
|
-
|
|
73
|
-
// Sample router-shaped files first (FastAPI / Flask), then views.py (Django),
|
|
74
|
-
// then any .py file as a last resort. Match on PATH, not just filename:
|
|
75
|
-
// FastAPI projects typically name routers by resource (`options.py`, `tax.py`)
|
|
76
|
-
// and place them under a `routers/` directory, so basename-only matching
|
|
77
|
-
// misses them. Also accept files where the basename itself is router-shaped
|
|
78
|
-
// (e.g., `endpoints.py`, `api.py`).
|
|
79
|
-
const routerFiles = sampleFiles(sourceDir, /\.py$/, (absPath, name) =>
|
|
80
|
-
/\/(routers?|api|endpoints?|views)\//.test(absPath) ||
|
|
81
|
-
/^(routers?|api|endpoints?)\.py$/.test(name),
|
|
82
|
-
);
|
|
83
|
-
const viewFiles = sampleFiles(sourceDir, /^views\.py$/);
|
|
84
|
-
const fallbackFiles = routerFiles.length === 0 && viewFiles.length === 0
|
|
85
|
-
? sampleFiles(sourceDir, /\.py$/)
|
|
86
|
-
: [];
|
|
87
|
-
const candidates = [...routerFiles, ...viewFiles, ...fallbackFiles].slice(
|
|
88
|
-
0,
|
|
89
|
-
MAX_SAMPLES_PER_ADAPTER,
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
if (candidates.length === 0) return null;
|
|
93
|
-
|
|
94
|
-
const authDeps = new Map<string, string>(); // value → first source path
|
|
95
|
-
const prefixBases = new Map<string, string>();
|
|
96
|
-
const testAsyncPatterns = new Map<string, string>();
|
|
97
|
-
|
|
98
|
-
for (const path of candidates) {
|
|
99
|
-
const body = readSafe(path);
|
|
100
|
-
if (body === null) continue;
|
|
101
|
-
|
|
102
|
-
// Auth dependency: `Depends(<name>)` — capture the call's argument.
|
|
103
|
-
// ReDoS-safe: anchored, short window, no nested quantifiers.
|
|
104
|
-
const authRegex = /\bDepends\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gu;
|
|
105
|
-
forEachMatch(authRegex, body, m => {
|
|
106
|
-
const name = m[1];
|
|
107
|
-
if (!authDeps.has(name)) authDeps.set(name, path);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Django: `@login_required` or `@permission_required` decorators.
|
|
111
|
-
const djangoAuthRegex = /^@\s*([a-z_][a-z0-9_]*(?:_required|_login))\b/gmu;
|
|
112
|
-
forEachMatch(djangoAuthRegex, body, m => {
|
|
113
|
-
const name = m[1];
|
|
114
|
-
if (!authDeps.has(name)) authDeps.set(name, path);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// API prefix base: `APIRouter(prefix="/api/...")` — capture just the BASE
|
|
118
|
-
// (everything up to the second `/` from the start).
|
|
119
|
-
const prefixRegex = /\bAPIRouter\s*\(\s*[^)]*?prefix\s*=\s*["']([^"']+)["']/gu;
|
|
120
|
-
forEachMatch(prefixRegex, body, m => {
|
|
121
|
-
const fullPrefix = m[1];
|
|
122
|
-
const base = extractPrefixBase(fullPrefix);
|
|
123
|
-
if (base && !prefixBases.has(base)) prefixBases.set(base, path);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Test async pattern: `@pytest.mark.asyncio` (with possible parens).
|
|
127
|
-
const asyncRegex = /^(@pytest\.mark\.asyncio(?:\s*\([^)]*\))?)/gmu;
|
|
128
|
-
forEachMatch(asyncRegex, body, m => {
|
|
129
|
-
const pat = m[1].trim();
|
|
130
|
-
if (!testAsyncPatterns.has(pat)) testAsyncPatterns.set(pat, path);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const authDep = pickBestSingleton(authDeps);
|
|
135
|
-
const apiPrefixBase = pickBestSingleton(prefixBases);
|
|
136
|
-
const testAsyncPattern = pickBestSingleton(testAsyncPatterns);
|
|
137
|
-
|
|
138
|
-
const result: DetectedPython = {};
|
|
139
|
-
const provenance: Record<string, string> = {};
|
|
140
|
-
|
|
141
|
-
if (authDep) {
|
|
142
|
-
result.auth_dep = authDep.value;
|
|
143
|
-
provenance.auth_dep_source = relativeTo(projectRoot, authDep.source);
|
|
144
|
-
}
|
|
145
|
-
if (apiPrefixBase) {
|
|
146
|
-
result.api_prefix_base = apiPrefixBase.value;
|
|
147
|
-
provenance.api_prefix_base_source = relativeTo(projectRoot, apiPrefixBase.source);
|
|
148
|
-
}
|
|
149
|
-
if (testAsyncPattern) {
|
|
150
|
-
result.test_async_pattern = testAsyncPattern.value;
|
|
151
|
-
provenance.test_async_pattern_source = relativeTo(projectRoot, testAsyncPattern.source);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Only emit a language block when at least one real field was extracted.
|
|
155
|
-
// An empty result (provenance-only) clutters the YAML for no value.
|
|
156
|
-
if (Object.keys(result).length === 0) return null;
|
|
157
|
-
if (Object.keys(provenance).length > 0) result._provenance = provenance;
|
|
158
|
-
return result;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Reduce `/api/foo/bar` to `/api`. Returns null if the path doesn't have at
|
|
163
|
-
* least one slash-segment.
|
|
164
|
-
*/
|
|
165
|
-
function extractPrefixBase(prefix: string): string | null {
|
|
166
|
-
if (!prefix.startsWith('/')) return null;
|
|
167
|
-
const stripped = prefix.replace(/^\/+/, '');
|
|
168
|
-
const firstSeg = stripped.split('/')[0];
|
|
169
|
-
if (!firstSeg) return null;
|
|
170
|
-
return '/' + firstSeg;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ============================================================
|
|
174
|
-
// Swift adapter
|
|
175
|
-
// ============================================================
|
|
176
|
-
|
|
177
|
-
export function introspectSwift(
|
|
178
|
-
detection: DetectionResult,
|
|
179
|
-
projectRoot: string,
|
|
180
|
-
): DetectedSwift | null {
|
|
181
|
-
const sourceDir = resolveSourceDir(detection, 'swift', projectRoot);
|
|
182
|
-
if (!sourceDir) return null;
|
|
183
|
-
|
|
184
|
-
// Prefer View files first, then any .swift. Path-aware: also match files
|
|
185
|
-
// under a `Views/` directory, since SwiftUI projects often name files by
|
|
186
|
-
// feature (e.g., `OrdersList.swift` inside `Features/Orders/Views/`).
|
|
187
|
-
const viewFiles = sampleFiles(sourceDir, /\.swift$/, (absPath, name) =>
|
|
188
|
-
/View\.swift$/.test(name) || /\/Views\//.test(absPath),
|
|
189
|
-
);
|
|
190
|
-
const fallbackFiles = viewFiles.length === 0
|
|
191
|
-
? sampleFiles(sourceDir, /\.swift$/)
|
|
192
|
-
: [];
|
|
193
|
-
const candidates = [...viewFiles, ...fallbackFiles].slice(
|
|
194
|
-
0,
|
|
195
|
-
MAX_SAMPLES_PER_ADAPTER,
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
if (candidates.length === 0) return null;
|
|
199
|
-
|
|
200
|
-
const apiClasses = new Map<string, string>();
|
|
201
|
-
const biometricPolicies = new Map<string, string>();
|
|
202
|
-
|
|
203
|
-
for (const path of candidates) {
|
|
204
|
-
const body = readSafe(path);
|
|
205
|
-
if (body === null) continue;
|
|
206
|
-
|
|
207
|
-
// API client class: looks like `let api = SomeAPI()` or
|
|
208
|
-
// `@StateObject var api: SomeAPI = .shared` etc.
|
|
209
|
-
// Extract `SomeAPI` from `SomeAPI(`/`SomeAPI.shared`/`: SomeAPI`.
|
|
210
|
-
const apiRegex = /\b([A-Z][A-Za-z0-9_]*API)\s*(?:\(|\.shared|\b)/gu;
|
|
211
|
-
forEachMatch(apiRegex, body, m => {
|
|
212
|
-
const name = m[1];
|
|
213
|
-
if (!apiClasses.has(name)) apiClasses.set(name, path);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// LocalAuthentication policy: `LAPolicy.<name>` or `.<name>` after
|
|
217
|
-
// `evaluatePolicy(`. Whitelist known good values to avoid false positives.
|
|
218
|
-
const policyRegex = /\.(deviceOwnerAuthentication(?:WithBiometrics)?)\b/gu;
|
|
219
|
-
forEachMatch(policyRegex, body, m => {
|
|
220
|
-
const name = m[1];
|
|
221
|
-
if (!biometricPolicies.has(name)) biometricPolicies.set(name, path);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const apiClass = pickBestSingleton(apiClasses);
|
|
226
|
-
const biometricPolicy = pickBestSingleton(biometricPolicies);
|
|
227
|
-
|
|
228
|
-
const result: DetectedSwift = {};
|
|
229
|
-
const provenance: Record<string, string> = {};
|
|
230
|
-
|
|
231
|
-
if (apiClass) {
|
|
232
|
-
result.api_client_class = apiClass.value;
|
|
233
|
-
provenance.api_client_class_source = relativeTo(projectRoot, apiClass.source);
|
|
234
|
-
}
|
|
235
|
-
if (biometricPolicy) {
|
|
236
|
-
result.biometric_policy = biometricPolicy.value;
|
|
237
|
-
provenance.biometric_policy_source = relativeTo(projectRoot, biometricPolicy.source);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (Object.keys(result).length === 0) return null;
|
|
241
|
-
if (Object.keys(provenance).length > 0) result._provenance = provenance;
|
|
242
|
-
return result;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ============================================================
|
|
246
|
-
// TypeScript / Next.js + tRPC adapter
|
|
247
|
-
// ============================================================
|
|
248
|
-
|
|
249
|
-
export function introspectTypeScript(
|
|
250
|
-
detection: DetectionResult,
|
|
251
|
-
projectRoot: string,
|
|
252
|
-
): DetectedTypeScript | null {
|
|
253
|
-
const sourceDir = resolveSourceDir(detection, 'typescript', projectRoot)
|
|
254
|
-
?? resolveSourceDir(detection, 'javascript', projectRoot);
|
|
255
|
-
if (!sourceDir) return null;
|
|
256
|
-
|
|
257
|
-
// Look for tRPC router files first. Match on PATH or filename: tRPC
|
|
258
|
-
// projects typically place routers under `server/api/routers/<name>.ts`
|
|
259
|
-
// where filenames are resource-named (e.g., `accounts.ts`).
|
|
260
|
-
const routerFiles = sampleFiles(sourceDir, /\.tsx?$/, (absPath, name) =>
|
|
261
|
-
/(router|trpc)/i.test(name) ||
|
|
262
|
-
/\/(routers|trpc|server\/api)\//.test(absPath),
|
|
263
|
-
);
|
|
264
|
-
const candidates = routerFiles.slice(0, MAX_SAMPLES_PER_ADAPTER);
|
|
265
|
-
|
|
266
|
-
if (candidates.length === 0) return null;
|
|
267
|
-
|
|
268
|
-
const builders = new Map<string, string>();
|
|
269
|
-
const procedurePatterns = new Map<string, string>();
|
|
270
|
-
|
|
271
|
-
for (const path of candidates) {
|
|
272
|
-
const body = readSafe(path);
|
|
273
|
-
if (body === null) continue;
|
|
274
|
-
|
|
275
|
-
// tRPC router builder: `createTRPCRouter({ ... })`. Whitelist known names
|
|
276
|
-
// (createTRPCRouter, router, t.router) — never grep for arbitrary identifiers.
|
|
277
|
-
const builderRegex = /\b(createTRPCRouter|router|t\.router)\s*\(/gu;
|
|
278
|
-
forEachMatch(builderRegex, body, m => {
|
|
279
|
-
const name = m[1];
|
|
280
|
-
if (!builders.has(name)) builders.set(name, path);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// Procedure pattern: `publicProcedure.input(...).query(...)` →
|
|
284
|
-
// capture just the procedure name (`publicProcedure`/`protectedProcedure`).
|
|
285
|
-
const procRegex = /\b([a-z]+Procedure)\b/gu;
|
|
286
|
-
forEachMatch(procRegex, body, m => {
|
|
287
|
-
const name = m[1];
|
|
288
|
-
if (!procedurePatterns.has(name)) procedurePatterns.set(name, path);
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const builder = pickBestSingleton(builders);
|
|
293
|
-
const proc = pickBestSingleton(procedurePatterns);
|
|
294
|
-
|
|
295
|
-
const result: DetectedTypeScript = {};
|
|
296
|
-
const provenance: Record<string, string> = {};
|
|
297
|
-
|
|
298
|
-
if (builder) {
|
|
299
|
-
result.trpc_router_builder = builder.value;
|
|
300
|
-
provenance.trpc_router_builder_source = relativeTo(projectRoot, builder.source);
|
|
301
|
-
}
|
|
302
|
-
if (proc) {
|
|
303
|
-
result.procedure_pattern = proc.value;
|
|
304
|
-
provenance.procedure_pattern_source = relativeTo(projectRoot, proc.source);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (Object.keys(result).length === 0) return null;
|
|
308
|
-
if (Object.keys(provenance).length > 0) result._provenance = provenance;
|
|
309
|
-
return result;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ============================================================
|
|
313
|
-
// Helpers
|
|
314
|
-
// ============================================================
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Resolve the dominant source directory for a language, falling back to the
|
|
318
|
-
* project root if detection didn't surface anything specific.
|
|
319
|
-
*/
|
|
320
|
-
function resolveSourceDir(
|
|
321
|
-
detection: DetectionResult,
|
|
322
|
-
lang: string,
|
|
323
|
-
projectRoot: string,
|
|
324
|
-
): string | null {
|
|
325
|
-
const dirs = (detection.sourceDirs as unknown as Record<string, { source_dirs?: string[] }>);
|
|
326
|
-
const info = dirs[lang];
|
|
327
|
-
const list = info?.source_dirs ?? [];
|
|
328
|
-
if (list.length > 0) {
|
|
329
|
-
const first = list[0];
|
|
330
|
-
const abs = resolve(projectRoot, first);
|
|
331
|
-
return existsSync(abs) ? abs : null;
|
|
332
|
-
}
|
|
333
|
-
return existsSync(projectRoot) ? projectRoot : null;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Walk `dir` and return up to MAX_SAMPLES_PER_ADAPTER files matching `nameRegex`,
|
|
338
|
-
* filtered further by `pathFilter` (called with absolute path AND basename, so
|
|
339
|
-
* filters can match either the filename (e.g., `views.py`) or a path
|
|
340
|
-
* component (e.g., a `routers/` parent directory). Skips dot-dirs,
|
|
341
|
-
* node_modules, .venv, etc. Bounded depth.
|
|
342
|
-
*/
|
|
343
|
-
export function sampleFiles(
|
|
344
|
-
dir: string,
|
|
345
|
-
nameRegex: RegExp,
|
|
346
|
-
pathFilter?: (absPath: string, basename: string) => boolean,
|
|
347
|
-
): string[] {
|
|
348
|
-
const out: string[] = [];
|
|
349
|
-
const stack: { path: string; depth: number }[] = [{ path: dir, depth: 0 }];
|
|
350
|
-
|
|
351
|
-
while (stack.length > 0 && out.length < MAX_SAMPLES_PER_ADAPTER * 4) {
|
|
352
|
-
const { path, depth } = stack.pop()!;
|
|
353
|
-
if (depth > MAX_DIR_DEPTH) continue;
|
|
354
|
-
|
|
355
|
-
let entries: string[];
|
|
356
|
-
try {
|
|
357
|
-
entries = readdirSync(path);
|
|
358
|
-
} catch {
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
for (const entry of entries) {
|
|
363
|
-
if (entry.startsWith('.')) continue;
|
|
364
|
-
if (entry === 'node_modules') continue;
|
|
365
|
-
if (entry === '__pycache__') continue;
|
|
366
|
-
if (entry === 'venv' || entry === '.venv') continue;
|
|
367
|
-
if (entry === 'dist' || entry === 'build') continue;
|
|
368
|
-
|
|
369
|
-
const child = join(path, entry);
|
|
370
|
-
let st;
|
|
371
|
-
try {
|
|
372
|
-
st = statSync(child);
|
|
373
|
-
} catch {
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (st.isDirectory()) {
|
|
378
|
-
stack.push({ path: child, depth: depth + 1 });
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (!nameRegex.test(entry)) continue;
|
|
383
|
-
if (pathFilter && !pathFilter(child, entry)) continue;
|
|
384
|
-
if (st.size > MAX_FILE_BYTES) continue;
|
|
385
|
-
out.push(child);
|
|
386
|
-
if (out.length >= MAX_SAMPLES_PER_ADAPTER * 4) break;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Stable ordering — deterministic test fixtures.
|
|
391
|
-
out.sort();
|
|
392
|
-
return out.slice(0, MAX_SAMPLES_PER_ADAPTER * 2);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/** Read a file as UTF-8. Returns null on any error or if too large. */
|
|
396
|
-
export function readSafe(path: string): string | null {
|
|
397
|
-
try {
|
|
398
|
-
const st = statSync(path);
|
|
399
|
-
if (st.size > MAX_FILE_BYTES) return null;
|
|
400
|
-
return readFileSync(path, 'utf-8');
|
|
401
|
-
} catch {
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Run a global regex over `body` and call `cb` for each match. Caps iteration
|
|
408
|
-
* at 1000 matches per regex to defend against pathological inputs.
|
|
409
|
-
*/
|
|
410
|
-
export function forEachMatch(
|
|
411
|
-
re: RegExp,
|
|
412
|
-
body: string,
|
|
413
|
-
cb: (m: RegExpExecArray) => void,
|
|
414
|
-
): void {
|
|
415
|
-
if (!re.global) return;
|
|
416
|
-
re.lastIndex = 0;
|
|
417
|
-
let count = 0;
|
|
418
|
-
let m: RegExpExecArray | null;
|
|
419
|
-
while ((m = re.exec(body)) !== null) {
|
|
420
|
-
cb(m);
|
|
421
|
-
count++;
|
|
422
|
-
if (count > 1000) break;
|
|
423
|
-
// Defensive: zero-width match → bump lastIndex to avoid infinite loop.
|
|
424
|
-
if (m.index === re.lastIndex) re.lastIndex++;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* If `samples` has exactly one distinct value, return it.
|
|
430
|
-
* If it has 2 values, return the first-seen (stable, deterministic order from
|
|
431
|
-
* file walk).
|
|
432
|
-
* If it has 3+ distinct values, return null — Risk #6 (auth-dep ambiguity).
|
|
433
|
-
*/
|
|
434
|
-
export function pickBestSingleton(
|
|
435
|
-
samples: Map<string, string>,
|
|
436
|
-
): { value: string; source: string } | null {
|
|
437
|
-
if (samples.size === 0) return null;
|
|
438
|
-
if (samples.size >= 3) return null;
|
|
439
|
-
const [firstKey, firstSource] = samples.entries().next().value as [string, string];
|
|
440
|
-
return { value: firstKey, source: firstSource };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/** Make a file path relative to the project root. */
|
|
444
|
-
export function relativeTo(projectRoot: string, absPath: string): string {
|
|
445
|
-
if (absPath.startsWith(projectRoot + '/')) {
|
|
446
|
-
return absPath.slice(projectRoot.length + 1);
|
|
447
|
-
}
|
|
448
|
-
return basename(absPath);
|
|
449
|
-
}
|