@rkarim08/sia 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +35 -0
- package/.claude-plugin/plugin.json +27 -0
- package/.mcp.json +13 -0
- package/CLAUDE.md +226 -0
- package/LICENSE +202 -0
- package/PLUGIN_README.md +253 -0
- package/README.md +1013 -0
- package/agents/sia-changelog-writer.md +89 -0
- package/agents/sia-code-reviewer.md +86 -0
- package/agents/sia-conflict-resolver.md +100 -0
- package/agents/sia-convention-enforcer.md +69 -0
- package/agents/sia-debug.md +106 -0
- package/agents/sia-decision-reviewer.md +101 -0
- package/agents/sia-dependency-tracker.md +80 -0
- package/agents/sia-explain.md +126 -0
- package/agents/sia-feature.md +116 -0
- package/agents/sia-knowledge-capture.md +117 -0
- package/agents/sia-lead-architecture-advisor.md +93 -0
- package/agents/sia-lead-team-health.md +107 -0
- package/agents/sia-migration.md +100 -0
- package/agents/sia-onboarding.md +115 -0
- package/agents/sia-orientation.md +99 -0
- package/agents/sia-pm-briefing.md +106 -0
- package/agents/sia-pm-risk-advisor.md +82 -0
- package/agents/sia-qa-analyst.md +116 -0
- package/agents/sia-qa-regression-map.md +94 -0
- package/agents/sia-refactor.md +115 -0
- package/agents/sia-regression.md +112 -0
- package/agents/sia-security-audit.md +125 -0
- package/agents/sia-test-advisor.md +91 -0
- package/hooks/hooks.json +98 -0
- package/migrations/bridge/001_initial.sql +34 -0
- package/migrations/episodic/001_initial.sql +35 -0
- package/migrations/meta/001_initial.sql +68 -0
- package/migrations/semantic/001_initial.sql +292 -0
- package/migrations/semantic/002_ontology.sql +89 -0
- package/migrations/semantic/003_freshness.sql +63 -0
- package/migrations/semantic/004_v5_unified_schema.sql +194 -0
- package/migrations/semantic/005_backfill_event_kinds.sql +8 -0
- package/migrations/semantic/006_tree_sitter.sql +6 -0
- package/migrations/semantic/007_branch_snapshots.sql +22 -0
- package/package.json +110 -0
- package/scripts/branch-switch.sh +13 -0
- package/scripts/build-wasm-grammars.sh +81 -0
- package/scripts/post-compact.sh +8 -0
- package/scripts/post-tool-use.sh +10 -0
- package/scripts/pre-compact.sh +8 -0
- package/scripts/session-end.sh +8 -0
- package/scripts/session-start.sh +8 -0
- package/scripts/start-mcp.ts +45 -0
- package/scripts/stop-hook.sh +8 -0
- package/scripts/user-prompt-submit.sh +8 -0
- package/scripts/viz-server.ts +152 -0
- package/skills/sia-brainstorm/SKILL.md +156 -0
- package/skills/sia-brainstorm/scripts/frame-template.html +214 -0
- package/skills/sia-brainstorm/scripts/helper.js +95 -0
- package/skills/sia-brainstorm/scripts/server.cjs +338 -0
- package/skills/sia-brainstorm/scripts/start-server.sh +153 -0
- package/skills/sia-brainstorm/scripts/stop-server.sh +55 -0
- package/skills/sia-brainstorm/spec-document-reviewer-prompt.md +49 -0
- package/skills/sia-brainstorm/visual-companion.md +286 -0
- package/skills/sia-capture/SKILL.md +64 -0
- package/skills/sia-compare/SKILL.md +33 -0
- package/skills/sia-conflicts/SKILL.md +38 -0
- package/skills/sia-debug-workflow/SKILL.md +120 -0
- package/skills/sia-debug-workflow/root-cause-tracing.md +70 -0
- package/skills/sia-debug-workflow/scripts/find-polluter.sh +64 -0
- package/skills/sia-debug-workflow/temporal-investigation.md +72 -0
- package/skills/sia-digest/SKILL.md +23 -0
- package/skills/sia-dispatch/SKILL.md +69 -0
- package/skills/sia-dispatch/agent-task-template.md +99 -0
- package/skills/sia-doctor/SKILL.md +39 -0
- package/skills/sia-execute/SKILL.md +70 -0
- package/skills/sia-execute-plan/SKILL.md +85 -0
- package/skills/sia-export-import/SKILL.md +49 -0
- package/skills/sia-export-knowledge/SKILL.md +46 -0
- package/skills/sia-finish/SKILL.md +100 -0
- package/skills/sia-finish/pr-summary-template.md +54 -0
- package/skills/sia-freshness/SKILL.md +38 -0
- package/skills/sia-history/SKILL.md +42 -0
- package/skills/sia-impact/SKILL.md +70 -0
- package/skills/sia-index/SKILL.md +54 -0
- package/skills/sia-install/SKILL.md +39 -0
- package/skills/sia-lead-compliance/SKILL.md +16 -0
- package/skills/sia-lead-drift-report/SKILL.md +16 -0
- package/skills/sia-lead-knowledge-map/SKILL.md +16 -0
- package/skills/sia-learn/SKILL.md +58 -0
- package/skills/sia-plan/SKILL.md +68 -0
- package/skills/sia-plan/plan-reviewer-prompt.md +63 -0
- package/skills/sia-playbooks/SKILL.md +29 -0
- package/skills/sia-playbooks/reference-feature.md +100 -0
- package/skills/sia-playbooks/reference-flagging.md +50 -0
- package/skills/sia-playbooks/reference-orientation.md +92 -0
- package/skills/sia-playbooks/reference-regression.md +115 -0
- package/skills/sia-playbooks/reference-review.md +64 -0
- package/skills/sia-playbooks/reference-tools.md +239 -0
- package/skills/sia-pm-decision-log/SKILL.md +28 -0
- package/skills/sia-pm-risk-dashboard/SKILL.md +24 -0
- package/skills/sia-pm-sprint-summary/SKILL.md +27 -0
- package/skills/sia-prune/SKILL.md +45 -0
- package/skills/sia-qa-coverage/SKILL.md +28 -0
- package/skills/sia-qa-flaky/SKILL.md +20 -0
- package/skills/sia-qa-report/SKILL.md +26 -0
- package/skills/sia-reindex/SKILL.md +30 -0
- package/skills/sia-review-respond/SKILL.md +88 -0
- package/skills/sia-review-respond/pushback-patterns.md +90 -0
- package/skills/sia-search/SKILL.md +47 -0
- package/skills/sia-setup/SKILL.md +82 -0
- package/skills/sia-setup/setup-checklist.md +97 -0
- package/skills/sia-stats/SKILL.md +36 -0
- package/skills/sia-status/SKILL.md +44 -0
- package/skills/sia-sync/SKILL.md +46 -0
- package/skills/sia-team/SKILL.md +64 -0
- package/skills/sia-test/SKILL.md +92 -0
- package/skills/sia-test/testing-anti-patterns.md +104 -0
- package/skills/sia-tour/SKILL.md +29 -0
- package/skills/sia-upgrade/SKILL.md +43 -0
- package/skills/sia-verify/SKILL.md +81 -0
- package/skills/sia-visualize/SKILL.md +28 -0
- package/skills/sia-visualize-live/SKILL.md +55 -0
- package/skills/sia-visualize-live/scripts/graph-template.html +389 -0
- package/skills/sia-visualize-live/scripts/start-visualizer.sh +161 -0
- package/skills/sia-visualize-live/scripts/stop-visualizer.sh +55 -0
- package/skills/sia-visualize-live/scripts/visualizer-server.cjs +264 -0
- package/skills/sia-workspace/SKILL.md +57 -0
- package/src/agent/claude-md-template-flagging.md +219 -0
- package/src/agent/claude-md-template.md +213 -0
- package/src/agent/modules/sia-feature.md +100 -0
- package/src/agent/modules/sia-flagging.md +50 -0
- package/src/agent/modules/sia-orientation.md +92 -0
- package/src/agent/modules/sia-regression.md +115 -0
- package/src/agent/modules/sia-review.md +64 -0
- package/src/agent/modules/sia-tools.md +239 -0
- package/src/ast/extractors/c-include.ts +189 -0
- package/src/ast/extractors/csharp-project.ts +260 -0
- package/src/ast/extractors/prisma-schema.ts +44 -0
- package/src/ast/extractors/project-manifest.ts +111 -0
- package/src/ast/extractors/sql-schema.ts +67 -0
- package/src/ast/extractors/tier-a.ts +423 -0
- package/src/ast/extractors/tier-b.ts +289 -0
- package/src/ast/extractors/tier-dispatch.ts +247 -0
- package/src/ast/index-worker.ts +108 -0
- package/src/ast/indexer.ts +484 -0
- package/src/ast/languages.ts +408 -0
- package/src/ast/pagerank-builder.ts +125 -0
- package/src/ast/path-utils.ts +137 -0
- package/src/ast/tree-sitter/backends/native.ts +57 -0
- package/src/ast/tree-sitter/backends/wasm.ts +39 -0
- package/src/ast/tree-sitter/call-walker.ts +44 -0
- package/src/ast/tree-sitter/edit-computer.ts +55 -0
- package/src/ast/tree-sitter/query-runner.ts +46 -0
- package/src/ast/tree-sitter/service.ts +174 -0
- package/src/ast/tree-sitter/tree-cache.ts +39 -0
- package/src/ast/tree-sitter/types.ts +79 -0
- package/src/ast/watcher.ts +322 -0
- package/src/capture/chunker.ts +169 -0
- package/src/capture/consolidate.ts +127 -0
- package/src/capture/edge-inferrer.ts +161 -0
- package/src/capture/embedder.ts +166 -0
- package/src/capture/embedding-cache.ts +73 -0
- package/src/capture/flag-processor.ts +64 -0
- package/src/capture/hook.ts +67 -0
- package/src/capture/pipeline.ts +450 -0
- package/src/capture/prompts/consolidate.ts +25 -0
- package/src/capture/prompts/edge-infer.ts +29 -0
- package/src/capture/prompts/extract-flagged.ts +36 -0
- package/src/capture/prompts/extract.ts +42 -0
- package/src/capture/tokenizer.ts +147 -0
- package/src/capture/track-a-ast.ts +93 -0
- package/src/capture/track-b-llm.ts +149 -0
- package/src/capture/types.ts +64 -0
- package/src/cli/commands/community.ts +137 -0
- package/src/cli/commands/compare.ts +123 -0
- package/src/cli/commands/conflicts.ts +41 -0
- package/src/cli/commands/digest.ts +197 -0
- package/src/cli/commands/disable-flagging.ts +34 -0
- package/src/cli/commands/doctor.ts +240 -0
- package/src/cli/commands/download-model.ts +161 -0
- package/src/cli/commands/enable-flagging.ts +34 -0
- package/src/cli/commands/export-knowledge.ts +208 -0
- package/src/cli/commands/export.ts +85 -0
- package/src/cli/commands/freshness.ts +164 -0
- package/src/cli/commands/graph.ts +51 -0
- package/src/cli/commands/history.ts +139 -0
- package/src/cli/commands/import.ts +335 -0
- package/src/cli/commands/install.ts +156 -0
- package/src/cli/commands/lead-report.ts +241 -0
- package/src/cli/commands/learn.ts +321 -0
- package/src/cli/commands/pm-report.ts +413 -0
- package/src/cli/commands/prune.ts +75 -0
- package/src/cli/commands/qa-report.ts +278 -0
- package/src/cli/commands/reindex.ts +104 -0
- package/src/cli/commands/rollback.ts +70 -0
- package/src/cli/commands/search.ts +103 -0
- package/src/cli/commands/server.ts +91 -0
- package/src/cli/commands/share.ts +33 -0
- package/src/cli/commands/stats.ts +79 -0
- package/src/cli/commands/status.ts +176 -0
- package/src/cli/commands/sync.ts +96 -0
- package/src/cli/commands/team.ts +118 -0
- package/src/cli/commands/tour.ts +157 -0
- package/src/cli/commands/visualize-live.ts +162 -0
- package/src/cli/commands/workspace.ts +117 -0
- package/src/cli/index.ts +424 -0
- package/src/cli/learn-progress.ts +87 -0
- package/src/community/detection-bridge.ts +344 -0
- package/src/community/leiden.ts +462 -0
- package/src/community/raptor.ts +210 -0
- package/src/community/scheduler.ts +74 -0
- package/src/community/summarize.ts +115 -0
- package/src/decay/archiver.ts +73 -0
- package/src/decay/bridge-orphan-cleanup.ts +212 -0
- package/src/decay/consolidation-sweep.ts +112 -0
- package/src/decay/decay.ts +116 -0
- package/src/decay/deep-validator.ts +62 -0
- package/src/decay/episodic-promoter.ts +132 -0
- package/src/decay/maintenance-scheduler.ts +326 -0
- package/src/decay/scheduler.ts +6 -0
- package/src/decay/session-sweeper.ts +79 -0
- package/src/decay/types.ts +17 -0
- package/src/freshness/confidence-decay.ts +122 -0
- package/src/freshness/cuckoo-filter.ts +176 -0
- package/src/freshness/deep-validation.ts +345 -0
- package/src/freshness/dirty-tracker.ts +237 -0
- package/src/freshness/file-watcher-layer.ts +119 -0
- package/src/freshness/firewall.ts +64 -0
- package/src/freshness/git-reconcile-layer.ts +161 -0
- package/src/freshness/inverted-index.ts +158 -0
- package/src/freshness/stale-read-layer.ts +222 -0
- package/src/graph/audit.ts +69 -0
- package/src/graph/bridge-db.ts +141 -0
- package/src/graph/communities.ts +195 -0
- package/src/graph/db-interface.ts +259 -0
- package/src/graph/edges.ts +163 -0
- package/src/graph/entities.ts +327 -0
- package/src/graph/episodic-db.ts +113 -0
- package/src/graph/flags.ts +31 -0
- package/src/graph/meta-db.ts +200 -0
- package/src/graph/semantic-db.ts +101 -0
- package/src/graph/session-resume.ts +56 -0
- package/src/graph/snapshots.ts +342 -0
- package/src/graph/staging.ts +151 -0
- package/src/graph/types.ts +128 -0
- package/src/hooks/adapters/claude-code.ts +21 -0
- package/src/hooks/adapters/cline.ts +43 -0
- package/src/hooks/adapters/cursor.ts +65 -0
- package/src/hooks/adapters/generic.ts +12 -0
- package/src/hooks/agent-detect.ts +34 -0
- package/src/hooks/claude-md-directives.ts +32 -0
- package/src/hooks/event-router.ts +182 -0
- package/src/hooks/extractors/pattern-detector.ts +111 -0
- package/src/hooks/handlers/post-compact.ts +30 -0
- package/src/hooks/handlers/post-tool-use.ts +403 -0
- package/src/hooks/handlers/pre-compact.ts +100 -0
- package/src/hooks/handlers/session-end.ts +47 -0
- package/src/hooks/handlers/session-start.ts +154 -0
- package/src/hooks/handlers/stop.ts +128 -0
- package/src/hooks/handlers/user-prompt-submit.ts +68 -0
- package/src/hooks/plugin-branch-switch.ts +68 -0
- package/src/hooks/plugin-common.ts +47 -0
- package/src/hooks/plugin-post-compact.ts +28 -0
- package/src/hooks/plugin-post-tool-use.ts +38 -0
- package/src/hooks/plugin-pre-compact.ts +37 -0
- package/src/hooks/plugin-session-end.ts +37 -0
- package/src/hooks/plugin-session-start.ts +75 -0
- package/src/hooks/plugin-stop.ts +61 -0
- package/src/hooks/plugin-user-prompt-submit.ts +47 -0
- package/src/hooks/types.ts +43 -0
- package/src/knowledge/discovery.ts +238 -0
- package/src/knowledge/external-refs.ts +98 -0
- package/src/knowledge/freshness.ts +221 -0
- package/src/knowledge/ingest.ts +330 -0
- package/src/knowledge/markdown-export.ts +229 -0
- package/src/knowledge/markdown-import.ts +359 -0
- package/src/knowledge/patterns.ts +74 -0
- package/src/knowledge/templates.ts +307 -0
- package/src/llm/ai-sdk-adapter.ts +46 -0
- package/src/llm/config.ts +88 -0
- package/src/llm/cost-tracker.ts +110 -0
- package/src/llm/prompts/extraction.ts +55 -0
- package/src/llm/prompts/summarization.ts +36 -0
- package/src/llm/prompts/validation.ts +37 -0
- package/src/llm/provider-registry.ts +68 -0
- package/src/llm/reliability.ts +179 -0
- package/src/llm/schemas.ts +52 -0
- package/src/mcp/freshness-annotator.ts +69 -0
- package/src/mcp/server.ts +949 -0
- package/src/mcp/tools/sia-ast-query.ts +225 -0
- package/src/mcp/tools/sia-at-time.ts +151 -0
- package/src/mcp/tools/sia-backlinks.ts +87 -0
- package/src/mcp/tools/sia-batch-execute.ts +169 -0
- package/src/mcp/tools/sia-by-file.ts +89 -0
- package/src/mcp/tools/sia-community.ts +113 -0
- package/src/mcp/tools/sia-doctor.ts +73 -0
- package/src/mcp/tools/sia-execute-file.ts +122 -0
- package/src/mcp/tools/sia-execute.ts +104 -0
- package/src/mcp/tools/sia-expand.ts +158 -0
- package/src/mcp/tools/sia-fetch-and-index.ts +241 -0
- package/src/mcp/tools/sia-flag.ts +65 -0
- package/src/mcp/tools/sia-index.ts +111 -0
- package/src/mcp/tools/sia-note.ts +134 -0
- package/src/mcp/tools/sia-search.ts +105 -0
- package/src/mcp/tools/sia-stats.ts +63 -0
- package/src/mcp/tools/sia-sync-status.ts +44 -0
- package/src/mcp/tools/sia-upgrade.ts +247 -0
- package/src/mcp/truncate.ts +231 -0
- package/src/native/bridge.ts +167 -0
- package/src/native/fallback-ast-diff.ts +144 -0
- package/src/native/fallback-graph.ts +325 -0
- package/src/ontology/constraints.ts +56 -0
- package/src/ontology/errors.ts +8 -0
- package/src/ontology/middleware.ts +266 -0
- package/src/retrieval/bm25-search.ts +151 -0
- package/src/retrieval/context-assembly.ts +76 -0
- package/src/retrieval/graph-traversal.ts +168 -0
- package/src/retrieval/pagerank.ts +40 -0
- package/src/retrieval/query-classifier.ts +106 -0
- package/src/retrieval/reranker.ts +156 -0
- package/src/retrieval/search.ts +236 -0
- package/src/retrieval/throttle.ts +102 -0
- package/src/retrieval/vector-search.ts +203 -0
- package/src/retrieval/workspace-search.ts +130 -0
- package/src/sandbox/context-mode.ts +285 -0
- package/src/sandbox/credential-pass.ts +55 -0
- package/src/sandbox/executor.ts +235 -0
- package/src/security/pattern-detector.ts +127 -0
- package/src/security/rule-of-two.ts +50 -0
- package/src/security/sanitize.ts +46 -0
- package/src/security/semantic-consistency.ts +93 -0
- package/src/security/staging-promoter.ts +154 -0
- package/src/shared/config.ts +302 -0
- package/src/shared/diagnostics.ts +210 -0
- package/src/shared/errors.ts +48 -0
- package/src/shared/git-utils.ts +143 -0
- package/src/shared/llm-client.ts +120 -0
- package/src/shared/logger.ts +99 -0
- package/src/shared/types.ts +79 -0
- package/src/sync/client.ts +43 -0
- package/src/sync/conflict.ts +106 -0
- package/src/sync/dedup.ts +183 -0
- package/src/sync/hlc.ts +117 -0
- package/src/sync/keychain.ts +144 -0
- package/src/sync/pull.ts +232 -0
- package/src/sync/push.ts +131 -0
- package/src/types/chokidar.d.ts +23 -0
- package/src/visualization/graph-renderer.ts +312 -0
- package/src/visualization/subgraph-extract.ts +208 -0
- package/src/visualization/views/community-clusters.ts +246 -0
- package/src/visualization/views/dependency-map.ts +189 -0
- package/src/visualization/views/graph-explorer.ts +364 -0
- package/src/visualization/views/timeline.ts +247 -0
- package/src/workspace/api-contracts.ts +226 -0
- package/src/workspace/cross-repo.ts +61 -0
- package/src/workspace/detector.ts +190 -0
- package/src/workspace/manifest.ts +141 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Module: discovery — Priority-ordered documentation file scanner
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { DISCOVERY_PATTERNS, type DiscoveryPattern, EXCLUDED_DIRS } from "@/knowledge/patterns";
|
|
6
|
+
|
|
7
|
+
export interface DiscoveredFile {
|
|
8
|
+
absolutePath: string;
|
|
9
|
+
relativePath: string;
|
|
10
|
+
pattern: DiscoveryPattern;
|
|
11
|
+
packagePath: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Manifest files that indicate a package root. */
|
|
15
|
+
const PACKAGE_MANIFESTS = ["package.json", "Cargo.toml", "go.mod", "pyproject.toml"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Discover documentation files in a repository root.
|
|
19
|
+
* Returns files sorted by priority (1 = highest).
|
|
20
|
+
* Excludes files in EXCLUDED_DIRS and respects .gitignore conceptually
|
|
21
|
+
* (by excluding common non-project directories).
|
|
22
|
+
*
|
|
23
|
+
* Also scans sub-package directories (detected by manifest files) so that
|
|
24
|
+
* monorepo packages have their documentation discovered automatically.
|
|
25
|
+
*/
|
|
26
|
+
export function discoverDocFiles(repoRoot: string): DiscoveredFile[] {
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
const results: DiscoveredFile[] = [];
|
|
29
|
+
|
|
30
|
+
const addMatches = (baseDir: string): void => {
|
|
31
|
+
for (const pattern of DISCOVERY_PATTERNS) {
|
|
32
|
+
const matches = resolvePattern(repoRoot, baseDir, pattern);
|
|
33
|
+
for (const file of matches) {
|
|
34
|
+
if (!seen.has(file.absolutePath)) {
|
|
35
|
+
seen.add(file.absolutePath);
|
|
36
|
+
results.push(file);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Scan repo root
|
|
43
|
+
addMatches(repoRoot);
|
|
44
|
+
|
|
45
|
+
// Scan sub-package directories for additional documentation
|
|
46
|
+
const subPackages = findSubPackageDirs(repoRoot);
|
|
47
|
+
for (const pkgDir of subPackages) {
|
|
48
|
+
addMatches(pkgDir);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
results.sort((a, b) => a.pattern.priority - b.pattern.priority);
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Scan a specific subdirectory for documentation files.
|
|
57
|
+
* Used for JIT discovery when the agent accesses files in a new directory.
|
|
58
|
+
*/
|
|
59
|
+
export function discoverDocFilesInDir(repoRoot: string, subDir: string): DiscoveredFile[] {
|
|
60
|
+
const absSubDir = resolve(repoRoot, subDir);
|
|
61
|
+
const seen = new Set<string>();
|
|
62
|
+
const results: DiscoveredFile[] = [];
|
|
63
|
+
|
|
64
|
+
for (const pattern of DISCOVERY_PATTERNS) {
|
|
65
|
+
const matches = resolvePattern(repoRoot, absSubDir, pattern);
|
|
66
|
+
for (const file of matches) {
|
|
67
|
+
if (!seen.has(file.absolutePath)) {
|
|
68
|
+
seen.add(file.absolutePath);
|
|
69
|
+
results.push(file);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
results.sort((a, b) => a.pattern.priority - b.pattern.priority);
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Recursively find directories containing package manifests within the repo,
|
|
80
|
+
* excluding the repo root itself and excluded directories.
|
|
81
|
+
* Limited to a reasonable depth (4 levels) to avoid deep traversals.
|
|
82
|
+
*/
|
|
83
|
+
function findSubPackageDirs(repoRoot: string, maxDepth = 4): string[] {
|
|
84
|
+
const absRoot = resolve(repoRoot);
|
|
85
|
+
const dirs: string[] = [];
|
|
86
|
+
|
|
87
|
+
function walk(dir: string, depth: number): void {
|
|
88
|
+
if (depth > maxDepth) return;
|
|
89
|
+
|
|
90
|
+
let entries: import("node:fs").Dirent[];
|
|
91
|
+
try {
|
|
92
|
+
entries = readdirSync(dir, { withFileTypes: true }) as import("node:fs").Dirent[];
|
|
93
|
+
} catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (!entry.isDirectory()) continue;
|
|
99
|
+
if (EXCLUDED_DIRS.has(entry.name as string)) continue;
|
|
100
|
+
|
|
101
|
+
const subDir = join(dir, entry.name as string);
|
|
102
|
+
|
|
103
|
+
// Check if this directory has a package manifest
|
|
104
|
+
const hasManifest = PACKAGE_MANIFESTS.some((m) => existsSync(join(subDir, m)));
|
|
105
|
+
if (hasManifest && subDir !== absRoot) {
|
|
106
|
+
dirs.push(subDir);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Continue walking regardless of manifest presence
|
|
110
|
+
walk(subDir, depth + 1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
walk(absRoot, 1);
|
|
115
|
+
return dirs;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Walk up from file's directory to repoRoot, looking for package manifest files.
|
|
120
|
+
* If found at a directory OTHER than repoRoot, returns the relative path of that
|
|
121
|
+
* directory. Returns null if file is at root level.
|
|
122
|
+
*/
|
|
123
|
+
function detectPackagePath(filePath: string, repoRoot: string): string | null {
|
|
124
|
+
const absRoot = resolve(repoRoot);
|
|
125
|
+
let current = resolve(dirname(filePath));
|
|
126
|
+
|
|
127
|
+
while (current.length >= absRoot.length) {
|
|
128
|
+
// Skip the repo root itself — we only want sub-packages
|
|
129
|
+
if (current === absRoot) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const manifest of PACKAGE_MANIFESTS) {
|
|
134
|
+
if (existsSync(join(current, manifest))) {
|
|
135
|
+
return relative(absRoot, current);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const parent = dirname(current);
|
|
140
|
+
if (parent === current) break;
|
|
141
|
+
current = parent;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check whether any path segment in a relative path is an excluded directory.
|
|
149
|
+
*/
|
|
150
|
+
function isExcludedPath(relativePath: string): boolean {
|
|
151
|
+
const segments = relativePath.split("/");
|
|
152
|
+
return segments.some((seg) => EXCLUDED_DIRS.has(seg));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve a single DiscoveryPattern against a base directory, returning
|
|
157
|
+
* all matching DiscoveredFile entries.
|
|
158
|
+
*
|
|
159
|
+
* Handles two kinds of patterns:
|
|
160
|
+
* - Direct file paths (no wildcards): e.g. "AGENTS.md", ".claude/CLAUDE.md"
|
|
161
|
+
* - Single-star globs: e.g. "docs/adr/*.md", ".cursor/rules/*.mdc"
|
|
162
|
+
*/
|
|
163
|
+
function resolvePattern(
|
|
164
|
+
repoRoot: string,
|
|
165
|
+
baseDir: string,
|
|
166
|
+
pattern: DiscoveryPattern,
|
|
167
|
+
): DiscoveredFile[] {
|
|
168
|
+
const results: DiscoveredFile[] = [];
|
|
169
|
+
const absRoot = resolve(repoRoot);
|
|
170
|
+
|
|
171
|
+
if (!pattern.glob.includes("*")) {
|
|
172
|
+
// Direct file — check existence
|
|
173
|
+
const absPath = resolve(baseDir, pattern.glob);
|
|
174
|
+
if (existsSync(absPath) && isFile(absPath)) {
|
|
175
|
+
const relPath = relative(absRoot, absPath);
|
|
176
|
+
if (!isExcludedPath(relPath)) {
|
|
177
|
+
results.push({
|
|
178
|
+
absolutePath: absPath,
|
|
179
|
+
relativePath: relPath,
|
|
180
|
+
pattern,
|
|
181
|
+
packagePath: detectPackagePath(absPath, repoRoot),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// Glob with wildcard — split into directory part and file filter
|
|
187
|
+
const starIdx = pattern.glob.indexOf("*");
|
|
188
|
+
const dirPart = pattern.glob.slice(0, starIdx);
|
|
189
|
+
const suffix = pattern.glob.slice(starIdx + 1);
|
|
190
|
+
|
|
191
|
+
// The directory containing the wildcard
|
|
192
|
+
const searchDir = resolve(baseDir, dirPart);
|
|
193
|
+
|
|
194
|
+
if (existsSync(searchDir) && isDirectory(searchDir)) {
|
|
195
|
+
let entries: import("node:fs").Dirent[];
|
|
196
|
+
try {
|
|
197
|
+
entries = readdirSync(searchDir, { withFileTypes: true }) as import("node:fs").Dirent[];
|
|
198
|
+
} catch {
|
|
199
|
+
return results;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const entry of entries) {
|
|
203
|
+
if (!entry.isFile()) continue;
|
|
204
|
+
if (!(entry.name as string).endsWith(suffix)) continue;
|
|
205
|
+
|
|
206
|
+
const absPath = join(searchDir, entry.name as string);
|
|
207
|
+
const relPath = relative(absRoot, absPath);
|
|
208
|
+
|
|
209
|
+
if (!isExcludedPath(relPath)) {
|
|
210
|
+
results.push({
|
|
211
|
+
absolutePath: absPath,
|
|
212
|
+
relativePath: relPath,
|
|
213
|
+
pattern,
|
|
214
|
+
packagePath: detectPackagePath(absPath, repoRoot),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isFile(p: string): boolean {
|
|
225
|
+
try {
|
|
226
|
+
return statSync(p).isFile();
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isDirectory(p: string): boolean {
|
|
233
|
+
try {
|
|
234
|
+
return statSync(p).isDirectory();
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External Reference Detection — detect external service URLs in documentation content.
|
|
3
|
+
*
|
|
4
|
+
* Scans text for URLs matching known external service patterns (Notion, Confluence,
|
|
5
|
+
* Jira, GitHub, etc.) and classifies them by service type.
|
|
6
|
+
*
|
|
7
|
+
* Security: external links are NEVER auto-followed. This module only detects
|
|
8
|
+
* and classifies URLs — it performs no network requests.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface ExternalServicePattern {
|
|
12
|
+
pattern: RegExp;
|
|
13
|
+
service: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DetectedExternalRef {
|
|
17
|
+
url: string;
|
|
18
|
+
service: string;
|
|
19
|
+
lineNumber: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Known external service patterns
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export const EXTERNAL_SERVICE_PATTERNS: ExternalServicePattern[] = [
|
|
27
|
+
{ pattern: /notion\.so\//, service: "notion" },
|
|
28
|
+
{ pattern: /[\w-]+\.atlassian\.net\/wiki\//, service: "confluence" },
|
|
29
|
+
{ pattern: /docs\.google\.com\//, service: "google-docs" },
|
|
30
|
+
{ pattern: /[\w-]+\.atlassian\.net\/browse\//, service: "jira" },
|
|
31
|
+
{ pattern: /linear\.app\//, service: "linear" },
|
|
32
|
+
{ pattern: /figma\.com\//, service: "figma" },
|
|
33
|
+
{ pattern: /miro\.com\//, service: "miro" },
|
|
34
|
+
{ pattern: /github\.com\/[\w-]+\/[\w-]+\/wiki/, service: "github-wiki" },
|
|
35
|
+
{ pattern: /github\.com\/[\w-]+\/[\w-]+\/issues\/\d+/, service: "github-issue" },
|
|
36
|
+
{ pattern: /github\.com\/[\w-]+\/[\w-]+\/pull\/\d+/, service: "github-pr" },
|
|
37
|
+
{ pattern: /stackoverflow\.com\/questions\/\d+/, service: "stackoverflow" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// URL extraction regex
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const URL_REGEX = /https?:\/\/[^\s)>\]"']+/g;
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Public API
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a URL matches any known external service pattern.
|
|
52
|
+
* Returns the service name if matched, null otherwise.
|
|
53
|
+
*/
|
|
54
|
+
export function classifyUrl(url: string): string | null {
|
|
55
|
+
for (const entry of EXTERNAL_SERVICE_PATTERNS) {
|
|
56
|
+
if (entry.pattern.test(url)) {
|
|
57
|
+
return entry.service;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Detect external service URLs in text content.
|
|
65
|
+
* Does NOT follow or fetch any URLs — only detects and classifies them.
|
|
66
|
+
*
|
|
67
|
+
* Scans each line for URLs matching known external service patterns.
|
|
68
|
+
* Returns all detected references with their line numbers and service types.
|
|
69
|
+
*/
|
|
70
|
+
export function detectExternalRefs(content: string): DetectedExternalRef[] {
|
|
71
|
+
if (!content || content.trim().length === 0) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const refs: DetectedExternalRef[] = [];
|
|
76
|
+
const lines = content.split("\n");
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
const line = lines[i];
|
|
80
|
+
const urls = line.match(URL_REGEX);
|
|
81
|
+
if (!urls) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const url of urls) {
|
|
86
|
+
const service = classifyUrl(url);
|
|
87
|
+
if (service !== null) {
|
|
88
|
+
refs.push({
|
|
89
|
+
url,
|
|
90
|
+
service,
|
|
91
|
+
lineNumber: i + 1,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return refs;
|
|
98
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Module: freshness — Documentation freshness tracking via git metadata
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
5
|
+
import { updateEntity } from "@/graph/entities";
|
|
6
|
+
|
|
7
|
+
export interface FreshnessResult {
|
|
8
|
+
entityId: string;
|
|
9
|
+
filePath: string;
|
|
10
|
+
docModifiedAt: number | null;
|
|
11
|
+
codeModifiedAt: number | null;
|
|
12
|
+
divergenceDays: number | null;
|
|
13
|
+
isStale: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FreshnessConfig {
|
|
17
|
+
divergenceThreshold: number;
|
|
18
|
+
freshnessPenalty: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_FRESHNESS_CONFIG: FreshnessConfig = {
|
|
22
|
+
divergenceThreshold: 90,
|
|
23
|
+
freshnessPenalty: 0.15,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Documentation tags that identify FileNode entities as documentation. */
|
|
27
|
+
const DOC_TAGS = ["ai-context", "architecture", "project-docs", "api-docs", "changelog"];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the last git modification date for a file.
|
|
31
|
+
* Returns Unix milliseconds or null if not tracked.
|
|
32
|
+
*
|
|
33
|
+
* Note: Uses execSync with git CLI. The filePath argument comes from Sia's
|
|
34
|
+
* internal graph database (entity file_paths column), not from external user
|
|
35
|
+
* input, so shell injection risk is minimal. The path is quoted in the command.
|
|
36
|
+
*/
|
|
37
|
+
export function getGitModifiedAt(repoRoot: string, filePath: string): number | null {
|
|
38
|
+
try {
|
|
39
|
+
const output = execSync(`git log -1 --format=%at -- "${filePath}"`, {
|
|
40
|
+
cwd: repoRoot,
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
timeout: 5000,
|
|
43
|
+
}).trim();
|
|
44
|
+
if (!output) return null;
|
|
45
|
+
return Number.parseInt(output, 10) * 1000;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check freshness of a documentation FileNode by comparing its last git modification
|
|
53
|
+
* date to the most recent modification of code files it references.
|
|
54
|
+
*
|
|
55
|
+
* Does NOT modify the database — returns freshness info for the caller to act on.
|
|
56
|
+
*/
|
|
57
|
+
export function checkDocFreshness(
|
|
58
|
+
repoRoot: string,
|
|
59
|
+
docFilePath: string,
|
|
60
|
+
referencedFilePaths: string[],
|
|
61
|
+
config?: FreshnessConfig,
|
|
62
|
+
): FreshnessResult {
|
|
63
|
+
const cfg = config ?? DEFAULT_FRESHNESS_CONFIG;
|
|
64
|
+
const docModifiedAt = getGitModifiedAt(repoRoot, docFilePath);
|
|
65
|
+
|
|
66
|
+
if (docModifiedAt === null || referencedFilePaths.length === 0) {
|
|
67
|
+
return {
|
|
68
|
+
entityId: "",
|
|
69
|
+
filePath: docFilePath,
|
|
70
|
+
docModifiedAt,
|
|
71
|
+
codeModifiedAt: null,
|
|
72
|
+
divergenceDays: null,
|
|
73
|
+
isStale: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let latestCodeModified: number | null = null;
|
|
78
|
+
for (const codePath of referencedFilePaths) {
|
|
79
|
+
const ts = getGitModifiedAt(repoRoot, codePath);
|
|
80
|
+
if (ts !== null && (latestCodeModified === null || ts > latestCodeModified)) {
|
|
81
|
+
latestCodeModified = ts;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (latestCodeModified === null) {
|
|
86
|
+
return {
|
|
87
|
+
entityId: "",
|
|
88
|
+
filePath: docFilePath,
|
|
89
|
+
docModifiedAt,
|
|
90
|
+
codeModifiedAt: null,
|
|
91
|
+
divergenceDays: null,
|
|
92
|
+
isStale: false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const msPerDay = 86_400_000;
|
|
97
|
+
const divergenceDays = (latestCodeModified - docModifiedAt) / msPerDay;
|
|
98
|
+
const isStale = divergenceDays > 0 && divergenceDays > cfg.divergenceThreshold;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
entityId: "",
|
|
102
|
+
filePath: docFilePath,
|
|
103
|
+
docModifiedAt,
|
|
104
|
+
codeModifiedAt: latestCodeModified,
|
|
105
|
+
divergenceDays,
|
|
106
|
+
isStale,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Run freshness check on all documentation FileNodes in the graph that have
|
|
112
|
+
* reference edges to code entities.
|
|
113
|
+
*
|
|
114
|
+
* Returns freshness results for all checked documents.
|
|
115
|
+
* Optionally applies freshness penalty to stale documents.
|
|
116
|
+
*/
|
|
117
|
+
export async function checkAllDocFreshness(
|
|
118
|
+
db: SiaDb,
|
|
119
|
+
repoRoot: string,
|
|
120
|
+
config?: FreshnessConfig,
|
|
121
|
+
opts?: { applyPenalty?: boolean },
|
|
122
|
+
): Promise<FreshnessResult[]> {
|
|
123
|
+
const cfg = config ?? DEFAULT_FRESHNESS_CONFIG;
|
|
124
|
+
|
|
125
|
+
// Build the LIKE conditions for documentation tags
|
|
126
|
+
const tagConditions = DOC_TAGS.map((tag) => `tags LIKE '%${tag}%'`).join(" OR ");
|
|
127
|
+
|
|
128
|
+
const docRows = await db.execute(
|
|
129
|
+
`SELECT id, file_paths, tags, importance FROM graph_nodes
|
|
130
|
+
WHERE type = 'FileNode'
|
|
131
|
+
AND (${tagConditions})
|
|
132
|
+
AND t_valid_until IS NULL AND archived_at IS NULL`,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const results: FreshnessResult[] = [];
|
|
136
|
+
|
|
137
|
+
for (const row of docRows.rows) {
|
|
138
|
+
const entityId = row.id as string;
|
|
139
|
+
const filePaths = parseJsonArray(row.file_paths as string);
|
|
140
|
+
const importance = row.importance as number;
|
|
141
|
+
|
|
142
|
+
if (filePaths.length === 0) continue;
|
|
143
|
+
|
|
144
|
+
const docFilePath = filePaths[0] as string;
|
|
145
|
+
|
|
146
|
+
// Find referenced code files via edges
|
|
147
|
+
const referencedPaths = await getReferencedCodePaths(db, entityId);
|
|
148
|
+
|
|
149
|
+
const result = checkDocFreshness(repoRoot, docFilePath, referencedPaths, cfg);
|
|
150
|
+
result.entityId = entityId;
|
|
151
|
+
|
|
152
|
+
if (opts?.applyPenalty && result.isStale) {
|
|
153
|
+
const currentTags = parseJsonArray(row.tags as string);
|
|
154
|
+
if (!currentTags.includes("potentially-stale")) {
|
|
155
|
+
currentTags.push("potentially-stale");
|
|
156
|
+
}
|
|
157
|
+
const newImportance = Math.max(0.01, importance - cfg.freshnessPenalty);
|
|
158
|
+
await updateEntity(db, entityId, {
|
|
159
|
+
tags: JSON.stringify(currentTags),
|
|
160
|
+
importance: newImportance,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
results.push(result);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Find all code file paths referenced by a documentation entity via edges.
|
|
172
|
+
* Includes both direct edges and edges from ContentChunk children.
|
|
173
|
+
*/
|
|
174
|
+
async function getReferencedCodePaths(db: SiaDb, docEntityId: string): Promise<string[]> {
|
|
175
|
+
// Direct edges from the doc entity to code entities
|
|
176
|
+
const directResult = await db.execute(
|
|
177
|
+
`SELECT DISTINCT e2.file_paths FROM graph_edges ed
|
|
178
|
+
JOIN graph_nodes e2 ON e2.id = ed.to_id
|
|
179
|
+
WHERE ed.from_id = ? AND ed.t_valid_until IS NULL
|
|
180
|
+
AND e2.type IN ('CodeEntity', 'FileNode')
|
|
181
|
+
AND e2.t_valid_until IS NULL AND e2.archived_at IS NULL`,
|
|
182
|
+
[docEntityId],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Edges from ContentChunk children of this doc entity
|
|
186
|
+
const chunkResult = await db.execute(
|
|
187
|
+
`SELECT DISTINCT e3.file_paths FROM graph_edges parent_edge
|
|
188
|
+
JOIN graph_nodes chunk ON chunk.id = parent_edge.to_id
|
|
189
|
+
JOIN graph_edges child_edge ON child_edge.from_id = chunk.id
|
|
190
|
+
JOIN graph_nodes e3 ON e3.id = child_edge.to_id
|
|
191
|
+
WHERE parent_edge.from_id = ? AND parent_edge.t_valid_until IS NULL
|
|
192
|
+
AND chunk.type = 'ContentChunk'
|
|
193
|
+
AND chunk.t_valid_until IS NULL AND chunk.archived_at IS NULL
|
|
194
|
+
AND child_edge.t_valid_until IS NULL
|
|
195
|
+
AND e3.type IN ('CodeEntity', 'FileNode')
|
|
196
|
+
AND e3.t_valid_until IS NULL AND e3.archived_at IS NULL`,
|
|
197
|
+
[docEntityId],
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const paths = new Set<string>();
|
|
201
|
+
|
|
202
|
+
for (const row of [...directResult.rows, ...chunkResult.rows]) {
|
|
203
|
+
const filePaths = parseJsonArray(row.file_paths as string);
|
|
204
|
+
for (const p of filePaths) {
|
|
205
|
+
paths.add(p);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return [...paths];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Safely parse a JSON string array, returning [] on failure. */
|
|
213
|
+
function parseJsonArray(json: string): string[] {
|
|
214
|
+
try {
|
|
215
|
+
const parsed: unknown = JSON.parse(json);
|
|
216
|
+
if (Array.isArray(parsed)) return parsed as string[];
|
|
217
|
+
return [];
|
|
218
|
+
} catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|