@geminix/gxpm 0.1.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/AGENTS.md +148 -0
- package/CANON.md +53 -0
- package/CLAUDE.md +60 -0
- package/CONTEXT.md +49 -0
- package/DEBUG.md +59 -0
- package/ISSUE_CONTEXT.md +25 -0
- package/README.md +143 -0
- package/VERSION +1 -0
- package/agents/cleanup-auditor/cleanup-auditor.md +56 -0
- package/agents/grill-master.md +26 -0
- package/agents/implementer.md +32 -0
- package/agents/review-army/accessibility-reviewer.md +54 -0
- package/agents/review-army/code-quality-reviewer.md +54 -0
- package/agents/review-army/security-reviewer.md +56 -0
- package/agents/review-army/spec-compliance-reviewer.md +51 -0
- package/agents/review-army/test-reviewer.md +55 -0
- package/agents/reviewer.md +59 -0
- package/agents/ship-audit-army/docs-auditor.md +53 -0
- package/agents/ship-audit-army/performance-auditor.md +52 -0
- package/agents/ship-audit-army/security-auditor.md +52 -0
- package/agents/specifier.md +55 -0
- package/agents/triage-officer.md +27 -0
- package/bin/gxpm +17 -0
- package/bin/gxpm-browser +17 -0
- package/bin/gxpm-config +15 -0
- package/bin/gxpm-eval +13 -0
- package/bin/gxpm-global-discover +15 -0
- package/bin/gxpm-init +38 -0
- package/bin/gxpm-investigate +194 -0
- package/bin/gxpm-uninstall +15 -0
- package/bin/gxpm-update-check +165 -0
- package/commands/build.md +40 -0
- package/commands/help.md +53 -0
- package/commands/plan.md +34 -0
- package/commands/refine.md +46 -0
- package/commands/review.md +34 -0
- package/commands/ship.md +37 -0
- package/core/ac-check.ts +20 -0
- package/core/agent-runtime.ts +363 -0
- package/core/artifact-validator.ts +151 -0
- package/core/artifacts.ts +313 -0
- package/core/autopilot.ts +250 -0
- package/core/capabilities.ts +779 -0
- package/core/checkpoint.ts +370 -0
- package/core/cleanup.ts +32 -0
- package/core/command-probe.ts +82 -0
- package/core/config.ts +533 -0
- package/core/contracts/behavior-spec.schema.ts +38 -0
- package/core/contracts/converter.ts +61 -0
- package/core/contracts/host.ts +43 -0
- package/core/converters/converter.ts +93 -0
- package/core/converters/index.ts +8 -0
- package/core/converters/managed-artifact.ts +119 -0
- package/core/converters/parser.ts +159 -0
- package/core/converters/template-renderer.ts +35 -0
- package/core/converters/writer.ts +61 -0
- package/core/dag-executor.ts +426 -0
- package/core/dag-loader.ts +292 -0
- package/core/dag-schemas.ts +150 -0
- package/core/dispatch.ts +125 -0
- package/core/evidence.ts +148 -0
- package/core/gate.ts +269 -0
- package/core/hook-engine.ts +566 -0
- package/core/host-probe.ts +64 -0
- package/core/implement.ts +16 -0
- package/core/isolation-errors.ts +174 -0
- package/core/isolation-resolver.ts +921 -0
- package/core/issue-context.ts +381 -0
- package/core/issue-readiness.ts +457 -0
- package/core/issue-sync.ts +427 -0
- package/core/issues.ts +132 -0
- package/core/land.ts +108 -0
- package/core/orchestrator.ts +54 -0
- package/core/phase-artifact.ts +32 -0
- package/core/phase-gates.ts +130 -0
- package/core/phase-rewind.ts +94 -0
- package/core/plan-lint.ts +61 -0
- package/core/plan.ts +77 -0
- package/core/port-allocation.ts +50 -0
- package/core/pr-check.ts +15 -0
- package/core/preset-system/preset-resolver.ts +221 -0
- package/core/project-init-status.ts +127 -0
- package/core/qa.ts +15 -0
- package/core/resilience.ts +165 -0
- package/core/runs.ts +288 -0
- package/core/safe-path.test.ts +80 -0
- package/core/safe-path.ts +60 -0
- package/core/sdd-gate.test.ts +98 -0
- package/core/sdd-gate.ts +134 -0
- package/core/self-review.ts +62 -0
- package/core/session.ts +70 -0
- package/core/ship.ts +86 -0
- package/core/specify.ts +173 -0
- package/core/state.ts +1002 -0
- package/core/template-engine.ts +152 -0
- package/core/template-resolver.test.ts +70 -0
- package/core/template-resolver.ts +156 -0
- package/core/triage.ts +26 -0
- package/core/verify.ts +15 -0
- package/core/wiki-native.ts +2423 -0
- package/core/wiki.ts +27 -0
- package/core/workflow-event-emitter.ts +163 -0
- package/core/workflows/engine.ts +273 -0
- package/core/workflows/expressions.ts +76 -0
- package/core/workflows/index.ts +38 -0
- package/core/workflows/steps/command.ts +43 -0
- package/core/workflows/steps/gate.ts +47 -0
- package/core/workflows/steps/gxpm.ts +44 -0
- package/core/workflows/steps/linear.ts +31 -0
- package/core/workflows/steps/shell.ts +65 -0
- package/core/workflows/types.ts +62 -0
- package/core/workspace-runtime.ts +227 -0
- package/core/worktree-init-steps.ts +647 -0
- package/core/worktree-init.ts +330 -0
- package/core/worktree-owner.ts +143 -0
- package/docs/GXPM_VERIFY.md +98 -0
- package/docs/INSTALL_FOR_AGENTS.md +113 -0
- package/docs/README.md +57 -0
- package/docs/adr/adr-005-multi-platform-skill-converter.md +72 -0
- package/docs/agents/domain.md +30 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +32 -0
- package/docs/architecture/gxpm-architecture-diagram.md +265 -0
- package/docs/architecture/gxpm-current-architecture.md +175 -0
- package/docs/architecture/gxpm-current-flow.md +278 -0
- package/docs/architecture/gxpm-replacement-architecture.md +211 -0
- package/docs/architecture/gxpm-target-architecture.md +449 -0
- package/docs/architecture/gxpm-v0-contract.md +311 -0
- package/docs/architecture/layered-workflow-boundaries.md +193 -0
- package/docs/architecture/preset-system.md +126 -0
- package/docs/architecture/scaffold-northstar.md +23 -0
- package/docs/brainstorms/2026-05-14-bdd-then-tdd-design.md +320 -0
- package/docs/brainstorms/README.md +22 -0
- package/docs/brainstorms/docs-knowledge-system-requirements.md +29 -0
- package/docs/governance/beta-skill-promotion.md +39 -0
- package/docs/governance/development-contract.md +144 -0
- package/docs/governance/gherkin-style.md +90 -0
- package/docs/governance/host-adapter.md +56 -0
- package/docs/governance/skill-authoring.md +87 -0
- package/docs/governance/skill-testing.md +356 -0
- package/docs/governance/template-authoring.md +53 -0
- package/docs/migrations/v0.2.md +51 -0
- package/docs/plans/README.md +23 -0
- package/docs/plans/bdd-then-tdd-plan.md +1767 -0
- package/docs/plans/docs-knowledge-system-plan.md +31 -0
- package/docs/plans/spec-kit-sdd-adoption-plan.md +305 -0
- package/docs/research/agents-md-best-practices.md +207 -0
- package/docs/research/archon-study.md +351 -0
- package/docs/research/claude-hooks-study.md +440 -0
- package/docs/research/codex-hooks-study.md +624 -0
- package/docs/research/everything-claude-code-study.md +252 -0
- package/docs/research/from-skills-to-layered-workflow.md +322 -0
- package/docs/research/gsd-study.md +69 -0
- package/docs/research/kimi-hooks-study.md +274 -0
- package/docs/research/mattpocock-skills-comparison.md +429 -0
- package/docs/research/mattpocock-skills-study.md +275 -0
- package/docs/research/oh-my-codex-study.md +279 -0
- package/docs/research/perplexity-agent-skills-design.md +168 -0
- package/docs/research/pmc-gstack-skill-study.md +122 -0
- package/docs/research/spec-kit-study.md +224 -0
- package/docs/research/superpowers-study.md +209 -0
- package/docs/roadmap/initial-roadmap.md +53 -0
- package/docs/solutions/README.md +45 -0
- package/docs/solutions/artifact-nesting-recovery.md +58 -0
- package/docs/solutions/session-context-restore-practice.md +67 -0
- package/docs/solutions/workflow/version-drift-recovery.md +49 -0
- package/docs/solutions/worktree-gate-recovery.md +62 -0
- package/docs/specs/README.md +28 -0
- package/docs/specs/claude.md +45 -0
- package/docs/specs/codex.md +44 -0
- package/docs/specs/cursor.md +44 -0
- package/hosts/adapters/claude.ts +29 -0
- package/hosts/adapters/codex.ts +27 -0
- package/hosts/adapters/cursor.ts +27 -0
- package/hosts/adapters/kimi.ts +27 -0
- package/hosts/claude.ts +23 -0
- package/hosts/codex.ts +26 -0
- package/hosts/cursor.ts +19 -0
- package/hosts/index.ts +33 -0
- package/hosts/registry.test.ts +52 -0
- package/hosts/registry.ts +57 -0
- package/hosts/schema.ts +58 -0
- package/package.json +52 -0
- package/scripts/browser.ts +185 -0
- package/scripts/cleanup.ts +142 -0
- package/scripts/commands/artifact.ts +115 -0
- package/scripts/commands/autopilot.ts +143 -0
- package/scripts/commands/capability.ts +57 -0
- package/scripts/commands/config.ts +69 -0
- package/scripts/commands/dag.ts +126 -0
- package/scripts/commands/feedback.ts +123 -0
- package/scripts/commands/gate.ts +291 -0
- package/scripts/commands/helpers.ts +126 -0
- package/scripts/commands/hook.ts +66 -0
- package/scripts/commands/init.ts +515 -0
- package/scripts/commands/issue.ts +825 -0
- package/scripts/commands/phase.ts +61 -0
- package/scripts/commands/preset.ts +159 -0
- package/scripts/commands/runtime.ts +199 -0
- package/scripts/commands/specify.ts +71 -0
- package/scripts/commands/upgrade.ts +243 -0
- package/scripts/commands/verify.ts +183 -0
- package/scripts/commands/wiki.ts +242 -0
- package/scripts/commands/workflow.ts +131 -0
- package/scripts/dev-skill.ts +55 -0
- package/scripts/discover-skills.ts +116 -0
- package/scripts/doctor.ts +410 -0
- package/scripts/dogfood-check.ts +125 -0
- package/scripts/eval-functional.ts +218 -0
- package/scripts/eval.ts +246 -0
- package/scripts/gen-skill-docs.ts +201 -0
- package/scripts/global-discover.ts +217 -0
- package/scripts/governance-check.ts +75 -0
- package/scripts/gxpm-check.ts +12 -0
- package/scripts/gxpm.ts +216 -0
- package/scripts/host-config.ts +62 -0
- package/scripts/install-claude-hooks.ts +138 -0
- package/scripts/install-codex-hooks.ts +271 -0
- package/scripts/install-hooks.ts +128 -0
- package/scripts/install-kimi-hooks.ts +92 -0
- package/scripts/install-skill.ts +184 -0
- package/scripts/phase-artifact-commands.ts +100 -0
- package/scripts/post-land-sync.ts +46 -0
- package/scripts/scaffold-check.ts +85 -0
- package/scripts/skill-naming-check.ts +78 -0
- package/scripts/skill-structure-check.ts +157 -0
- package/scripts/skills-lock-check.ts +60 -0
- package/scripts/sync-markdown-artifacts.ts +172 -0
- package/scripts/uninstall.ts +162 -0
- package/scripts/version.ts +47 -0
- package/scripts/wait-pr-ready.ts +407 -0
- package/skills/gxpm/SKILL.md +485 -0
- package/skills/gxpm/SKILL.md.tmpl +422 -0
- package/skills/gxpm/references/CANON.md +53 -0
- package/skills/gxpm/references/key-rules.md +130 -0
- package/skills/gxpm-architecture/SKILL.md +106 -0
- package/skills/gxpm-architecture/references/DEEPENING.md +37 -0
- package/skills/gxpm-architecture/references/INTERFACE-DESIGN.md +44 -0
- package/skills/gxpm-autopilot/SKILL.md +116 -0
- package/skills/gxpm-autopilot/SKILL.md.tmpl +107 -0
- package/skills/gxpm-browser/SKILL.md +105 -0
- package/skills/gxpm-browser/SKILL.md.tmpl +41 -0
- package/skills/gxpm-browser/references/commands.md +43 -0
- package/skills/gxpm-browser/references/evidence-path.md +20 -0
- package/skills/gxpm-build/SKILL.md +78 -0
- package/skills/gxpm-cleanup/SKILL.md +76 -0
- package/skills/gxpm-debug-issue/SKILL.md +39 -0
- package/skills/gxpm-diagnose/SKILL.md +220 -0
- package/skills/gxpm-diagnose/SKILL.md.tmpl +31 -0
- package/skills/gxpm-diagnose/references/feedback-loop.md +34 -0
- package/skills/gxpm-diagnose/references/feedback-loops.md +43 -0
- package/skills/gxpm-diagnose/references/phases.md +60 -0
- package/skills/gxpm-eval/SKILL.md +78 -0
- package/skills/gxpm-explore-codebase/SKILL.md +36 -0
- package/skills/gxpm-explore-codebase/scripts/summarize-communities.ts +51 -0
- package/skills/gxpm-feedback/SKILL.md +122 -0
- package/skills/gxpm-grill/SKILL.md +159 -0
- package/skills/gxpm-grill/SKILL.md.tmpl +77 -0
- package/skills/gxpm-grill/references/documentation-templates.md +56 -0
- package/skills/gxpm-grill/references/process.md +25 -0
- package/skills/gxpm-handoff/SKILL.md +112 -0
- package/skills/gxpm-hygiene/SKILL.md +69 -0
- package/skills/gxpm-implementer/SKILL.md +142 -0
- package/skills/gxpm-implementer/SKILL.md.tmpl +141 -0
- package/skills/gxpm-linear/SKILL.md +282 -0
- package/skills/gxpm-linear/SKILL.md.tmpl +86 -0
- package/skills/gxpm-linear/references/commands.md +75 -0
- package/skills/gxpm-linear/references/workflows.md +120 -0
- package/skills/gxpm-planning/SKILL.md +134 -0
- package/skills/gxpm-prototype/SKILL.md +64 -0
- package/skills/gxpm-refactor-safely/SKILL.md +62 -0
- package/skills/gxpm-review-army/SKILL.md +117 -0
- package/skills/gxpm-review-changes/SKILL.md +36 -0
- package/skills/gxpm-setup/SKILL.md +101 -0
- package/skills/gxpm-specifier/SKILL.md +135 -0
- package/skills/gxpm-tdd/SKILL.md +187 -0
- package/skills/gxpm-tdd/references/interface-design.md +23 -0
- package/skills/gxpm-tdd/references/mocking.md +27 -0
- package/skills/gxpm-tdd/references/red-green-refactor.md +61 -0
- package/skills/gxpm-tdd/references/troubleshooting.md +28 -0
- package/skills/gxpm-tdd/references/workflow.md +50 -0
- package/skills/gxpm-tdd/testing-anti-patterns.tmpl +304 -0
- package/skills/gxpm-triage/SKILL.md +160 -0
- package/skills/gxpm-verify/SKILL.md +107 -0
- package/skills/gxpm-write-skill/SKILL.md +131 -0
- package/skills/gxpm-zoom-out/SKILL.md +69 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md +54 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md.tmpl +53 -0
- package/templates/constitution-template.md +63 -0
- package/templates/hooks/gxpm-commit-msg +16 -0
- package/templates/hooks/gxpm-post-checkout +19 -0
- package/templates/hooks/gxpm-post-commit +7 -0
- package/templates/hooks/gxpm-post-merge +29 -0
- package/templates/hooks/gxpm-pre-commit +39 -0
- package/templates/hooks/gxpm-pre-push +33 -0
- package/templates/plan-template.md.tmpl +46 -0
- package/templates/spec-template.md.tmpl +63 -0
- package/templates/specify-stub.tmpl +22 -0
- package/templates/tasks-template.md.tmpl +32 -0
|
@@ -0,0 +1,2423 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
openSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { dirname, extname, join, relative, sep } from "node:path";
|
|
14
|
+
import ts from "typescript";
|
|
15
|
+
import { listArtifacts, readArtifact, type ArtifactType } from "./artifacts";
|
|
16
|
+
import { GXPM_PHASES, isGxpmPhase, readIssueState, type GxpmPhase } from "./state";
|
|
17
|
+
|
|
18
|
+
const NATIVE_WIKI_ROOT = ".gxpm/wiki";
|
|
19
|
+
const NATIVE_WIKI_STATE_PATH = ".gxpm/wiki/state.json";
|
|
20
|
+
const NATIVE_WIKI_INDEX_PATH = ".gxpm/wiki/index/files.json";
|
|
21
|
+
const NATIVE_WIKI_GRAPH_PATH = ".gxpm/wiki/index/graph.json";
|
|
22
|
+
const NATIVE_WIKI_DIMENSIONS_PATH = ".gxpm/wiki/index/dimensions.json";
|
|
23
|
+
const NATIVE_WIKI_CONTENT_ROOT = ".gxpm/wiki/content";
|
|
24
|
+
const NATIVE_WIKI_DOC_MANIFEST_PATH = ".gxpm/wiki/content/generated-docs.json";
|
|
25
|
+
const NATIVE_WIKI_MODULE_TREE_PATH = ".gxpm/wiki/index/module_tree.json";
|
|
26
|
+
const NATIVE_WIKI_PROJECT_TOPIC_DIR = "project-topics";
|
|
27
|
+
const NATIVE_WIKI_CONFIG_PATH = ".gxpm/wiki.json";
|
|
28
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
29
|
+
const MAX_TOP_PAGES = 8;
|
|
30
|
+
const NATIVE_MAX_FILE_BYTES = 1_000_000;
|
|
31
|
+
const NATIVE_IMPORT_RESOLVABLE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".md"];
|
|
32
|
+
const NATIVE_TEXT_EXTENSIONS = new Set([
|
|
33
|
+
"",
|
|
34
|
+
...NATIVE_IMPORT_RESOLVABLE_EXTENSIONS,
|
|
35
|
+
".sh",
|
|
36
|
+
".toml",
|
|
37
|
+
".txt",
|
|
38
|
+
".yaml",
|
|
39
|
+
".yml",
|
|
40
|
+
]);
|
|
41
|
+
const NATIVE_SKIP_DIRS = new Set([
|
|
42
|
+
".claude",
|
|
43
|
+
".codex",
|
|
44
|
+
".git",
|
|
45
|
+
".gxpm",
|
|
46
|
+
".qoder",
|
|
47
|
+
".turbo",
|
|
48
|
+
"coverage",
|
|
49
|
+
"dist",
|
|
50
|
+
"node_modules",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
export interface WikiPageSummary {
|
|
54
|
+
path: string;
|
|
55
|
+
title: string;
|
|
56
|
+
citedFiles: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface NativeWikiFileEntry {
|
|
60
|
+
path: string;
|
|
61
|
+
language: string;
|
|
62
|
+
sizeBytes: number;
|
|
63
|
+
mtimeMs: number;
|
|
64
|
+
lineCount?: number;
|
|
65
|
+
exports: string[];
|
|
66
|
+
imports: string[];
|
|
67
|
+
headings: string[];
|
|
68
|
+
symbols: Array<{ name: string; kind: string; line: number }>;
|
|
69
|
+
contentHash: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface NativeWikiIndex {
|
|
73
|
+
schemaVersion: 1;
|
|
74
|
+
provider: "gxpm";
|
|
75
|
+
generatedAt: string;
|
|
76
|
+
files: NativeWikiFileEntry[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface NativeWikiIndexSnapshot {
|
|
80
|
+
index: NativeWikiIndex;
|
|
81
|
+
contents: Map<string, string>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface NativeWikiGraphEdge {
|
|
85
|
+
from: string;
|
|
86
|
+
to: string;
|
|
87
|
+
kind: "imports";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface NativeWikiGraph {
|
|
91
|
+
schemaVersion: 1;
|
|
92
|
+
provider: "gxpm";
|
|
93
|
+
generatedAt: string;
|
|
94
|
+
nodes: Array<{ path: string; language: string }>;
|
|
95
|
+
edges: NativeWikiGraphEdge[];
|
|
96
|
+
unresolvedImports: Array<{ from: string; specifier: string }>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface NativeWikiFileDimensions {
|
|
100
|
+
path: string;
|
|
101
|
+
language: string;
|
|
102
|
+
dimensions: {
|
|
103
|
+
structure: string[];
|
|
104
|
+
symbols: string[];
|
|
105
|
+
apis: string[];
|
|
106
|
+
workflows: string[];
|
|
107
|
+
config: string[];
|
|
108
|
+
tests: string[];
|
|
109
|
+
docs: string[];
|
|
110
|
+
relations: string[];
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface NativeWikiDimensions {
|
|
115
|
+
schemaVersion: 1;
|
|
116
|
+
provider: "gxpm";
|
|
117
|
+
generatedAt: string;
|
|
118
|
+
files: NativeWikiFileDimensions[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface NativeWikiState {
|
|
122
|
+
schemaVersion: 1;
|
|
123
|
+
provider: "gxpm";
|
|
124
|
+
status: "idle" | "updating" | "queued";
|
|
125
|
+
baseCommit: string | null;
|
|
126
|
+
generatedAt: string;
|
|
127
|
+
indexPath: string;
|
|
128
|
+
graphPath: string;
|
|
129
|
+
dimensionsPath: string;
|
|
130
|
+
contentRoot: string;
|
|
131
|
+
queuedCommit: string | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface NativeWikiStatus {
|
|
135
|
+
schemaVersion: 1;
|
|
136
|
+
provider: "gxpm";
|
|
137
|
+
detected: boolean;
|
|
138
|
+
state: "absent" | "current" | "stale";
|
|
139
|
+
stale: boolean;
|
|
140
|
+
reason: string;
|
|
141
|
+
baseCommit: string | null;
|
|
142
|
+
currentCommit: string | null;
|
|
143
|
+
generatedAt?: string;
|
|
144
|
+
indexedFiles: number;
|
|
145
|
+
graphEdges: number;
|
|
146
|
+
dimensionedFiles: number;
|
|
147
|
+
docs: string[];
|
|
148
|
+
changedFiles: string[];
|
|
149
|
+
paths: {
|
|
150
|
+
state: string;
|
|
151
|
+
index: string;
|
|
152
|
+
graph: string;
|
|
153
|
+
dimensions: string;
|
|
154
|
+
contentRoot: string;
|
|
155
|
+
};
|
|
156
|
+
commands: {
|
|
157
|
+
init: string;
|
|
158
|
+
update: string;
|
|
159
|
+
query: string;
|
|
160
|
+
context: string;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface NativeWikiBuildResult {
|
|
165
|
+
provider: "gxpm";
|
|
166
|
+
mode: "init" | "update";
|
|
167
|
+
state: NativeWikiState;
|
|
168
|
+
index: NativeWikiIndex;
|
|
169
|
+
graph: NativeWikiGraph;
|
|
170
|
+
dimensions: NativeWikiDimensions;
|
|
171
|
+
docs: string[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface NativeWikiQueryResult {
|
|
175
|
+
provider: "gxpm";
|
|
176
|
+
query: string;
|
|
177
|
+
results: Array<{
|
|
178
|
+
path: string;
|
|
179
|
+
source: "file-index";
|
|
180
|
+
score: number;
|
|
181
|
+
matches: string[];
|
|
182
|
+
line?: number;
|
|
183
|
+
}>;
|
|
184
|
+
contextFiles: string[];
|
|
185
|
+
suggestedDocs: string[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface NativeWikiIssueContext {
|
|
189
|
+
schemaVersion: 1;
|
|
190
|
+
provider: "gxpm";
|
|
191
|
+
issueId: string;
|
|
192
|
+
currentPhase: GxpmPhase;
|
|
193
|
+
phase: GxpmPhase;
|
|
194
|
+
query: string;
|
|
195
|
+
artifactsUsed: Array<{
|
|
196
|
+
type: ArtifactType;
|
|
197
|
+
writtenAt: string;
|
|
198
|
+
}>;
|
|
199
|
+
results: NativeWikiQueryResult["results"];
|
|
200
|
+
contextFiles: string[];
|
|
201
|
+
suggestedDocs: string[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface NativeWikiConfig {
|
|
205
|
+
repo_notes?: string[];
|
|
206
|
+
priority_dirs?: string[];
|
|
207
|
+
exclude_from_map?: string[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface NativeWikiModuleTree {
|
|
211
|
+
schemaVersion: 1;
|
|
212
|
+
provider: "gxpm";
|
|
213
|
+
generatedAt: string;
|
|
214
|
+
root: {
|
|
215
|
+
name: string;
|
|
216
|
+
children: NativeWikiModuleTreeNode[];
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface NativeWikiModuleTreeNode {
|
|
221
|
+
name: string;
|
|
222
|
+
type: "file" | "directory";
|
|
223
|
+
children?: NativeWikiModuleTreeNode[];
|
|
224
|
+
fileCount?: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface NativeWikiEvalReport {
|
|
228
|
+
schemaVersion: 1;
|
|
229
|
+
provider: "gxpm";
|
|
230
|
+
generatedAt: string;
|
|
231
|
+
native: {
|
|
232
|
+
status: NativeWikiStatus;
|
|
233
|
+
generatedDocs: {
|
|
234
|
+
count: number;
|
|
235
|
+
names: string[];
|
|
236
|
+
};
|
|
237
|
+
projectTopics: {
|
|
238
|
+
clusterCount: number;
|
|
239
|
+
clusterTitles: string[];
|
|
240
|
+
};
|
|
241
|
+
sourceCoverage: {
|
|
242
|
+
indexedFiles: number;
|
|
243
|
+
dimensionedFiles: number;
|
|
244
|
+
graphEdges: number;
|
|
245
|
+
docsWithSourceAnchors: number;
|
|
246
|
+
anchoredFiles: number;
|
|
247
|
+
orphanIndexedFiles: number;
|
|
248
|
+
orphanIndexedFileExamples: string[];
|
|
249
|
+
};
|
|
250
|
+
queryScenarios: Array<{
|
|
251
|
+
query: string;
|
|
252
|
+
topFiles: string[];
|
|
253
|
+
suggestedDocs: string[];
|
|
254
|
+
}>;
|
|
255
|
+
};
|
|
256
|
+
recommendations: string[];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const DEFAULT_NATIVE_WIKI_EVAL_QUERIES = [
|
|
260
|
+
"phase gate artifact lifecycle",
|
|
261
|
+
"host adapter codex",
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
export function initializeNativeWiki(input: { root?: string; now?: Date } = {}): NativeWikiBuildResult {
|
|
265
|
+
return writeNativeWiki({ root: input.root, now: input.now, mode: "init" });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function updateNativeWiki(input: { root?: string; now?: Date } = {}): NativeWikiBuildResult {
|
|
269
|
+
return updateNativeWikiIncremental({ root: input.root, now: input.now });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function ensureNativeWikiCurrent(input: { root?: string; autoUpdate?: boolean } = {}): void {
|
|
273
|
+
if (input.autoUpdate === false) return;
|
|
274
|
+
if (process.env.GXPM_WIKI_AUTO_UPDATE === "0") return;
|
|
275
|
+
const root = input.root ?? process.cwd();
|
|
276
|
+
const status = getNativeWikiStatus({ root });
|
|
277
|
+
if (status.stale) {
|
|
278
|
+
updateNativeWiki({ root });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function getNativeWikiStatus(input: { root?: string; now?: Date } = {}): NativeWikiStatus {
|
|
283
|
+
const root = input.root ?? process.cwd();
|
|
284
|
+
const currentCommit = currentGitCommit(root);
|
|
285
|
+
const paths = nativeWikiStatusPaths();
|
|
286
|
+
const commands = nativeWikiStatusCommands();
|
|
287
|
+
const state = readNativeWikiStateIfPresent(root);
|
|
288
|
+
const index = readNativeWikiIndexIfPresent(root);
|
|
289
|
+
const graph = readNativeWikiGraphIfPresent(root);
|
|
290
|
+
const dimensions = readNativeWikiDimensionsIfPresent(root);
|
|
291
|
+
const docs = listNativeWikiDocs(root);
|
|
292
|
+
|
|
293
|
+
const missingArtifacts: string[] = [];
|
|
294
|
+
if (!state) missingArtifacts.push("state.json missing or unreadable");
|
|
295
|
+
if (!index) missingArtifacts.push("index/files.json missing or unreadable");
|
|
296
|
+
if (!graph) missingArtifacts.push("index/graph.json missing or unreadable");
|
|
297
|
+
if (!dimensions) missingArtifacts.push("index/dimensions.json missing or unreadable");
|
|
298
|
+
if (missingArtifacts.length > 0) {
|
|
299
|
+
const hasAnyArtifacts = !!state || !!index || !!graph || !!dimensions || docs.length > 0;
|
|
300
|
+
return {
|
|
301
|
+
schemaVersion: 1,
|
|
302
|
+
provider: "gxpm",
|
|
303
|
+
detected: hasAnyArtifacts,
|
|
304
|
+
state: hasAnyArtifacts ? "stale" : "absent",
|
|
305
|
+
stale: true,
|
|
306
|
+
reason: hasAnyArtifacts ? missingArtifacts.join("; ") : "Native gxpm wiki has not been initialized.",
|
|
307
|
+
baseCommit: state?.baseCommit ?? null,
|
|
308
|
+
currentCommit,
|
|
309
|
+
generatedAt: state?.generatedAt,
|
|
310
|
+
indexedFiles: index?.files.length ?? 0,
|
|
311
|
+
graphEdges: graph?.edges.length ?? 0,
|
|
312
|
+
dimensionedFiles: dimensions?.files.length ?? 0,
|
|
313
|
+
docs,
|
|
314
|
+
changedFiles: index ? changedNativeFiles(root, index) : [],
|
|
315
|
+
paths,
|
|
316
|
+
commands,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const changedFiles = changedNativeFiles(root, index);
|
|
321
|
+
const reasons: string[] = [];
|
|
322
|
+
if (state.baseCommit !== currentCommit) {
|
|
323
|
+
reasons.push("baseCommit differs from current HEAD");
|
|
324
|
+
}
|
|
325
|
+
if (changedFiles.length > 0) {
|
|
326
|
+
reasons.push("tracked files changed after generation");
|
|
327
|
+
}
|
|
328
|
+
const stale = reasons.length > 0;
|
|
329
|
+
return {
|
|
330
|
+
schemaVersion: 1,
|
|
331
|
+
provider: "gxpm",
|
|
332
|
+
detected: true,
|
|
333
|
+
state: stale ? "stale" : "current",
|
|
334
|
+
stale,
|
|
335
|
+
reason: stale ? reasons.join("; ") : "Native gxpm wiki is current for the working tree.",
|
|
336
|
+
baseCommit: state.baseCommit,
|
|
337
|
+
currentCommit,
|
|
338
|
+
generatedAt: state.generatedAt,
|
|
339
|
+
indexedFiles: index.files.length,
|
|
340
|
+
graphEdges: graph?.edges.length ?? 0,
|
|
341
|
+
dimensionedFiles: dimensions.files.length,
|
|
342
|
+
docs,
|
|
343
|
+
changedFiles,
|
|
344
|
+
paths,
|
|
345
|
+
commands,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function buildNativeWikiIndex(input: { root?: string; now?: Date } = {}): NativeWikiIndex {
|
|
350
|
+
return buildNativeWikiIndexSnapshot(input).index;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function buildNativeWikiIndexSnapshot(input: { root?: string; now?: Date } = {}): NativeWikiIndexSnapshot {
|
|
354
|
+
const root = input.root ?? process.cwd();
|
|
355
|
+
const generatedAt = (input.now ?? new Date()).toISOString();
|
|
356
|
+
const contents = new Map<string, string>();
|
|
357
|
+
return {
|
|
358
|
+
index: {
|
|
359
|
+
schemaVersion: 1,
|
|
360
|
+
provider: "gxpm",
|
|
361
|
+
generatedAt,
|
|
362
|
+
files: listNativeRepoFiles(root).map((file) => summarizeNativeFile(root, file, contents)),
|
|
363
|
+
},
|
|
364
|
+
contents,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function queryNativeWiki(input: {
|
|
369
|
+
root?: string;
|
|
370
|
+
query: string;
|
|
371
|
+
limit?: number;
|
|
372
|
+
autoUpdate?: boolean;
|
|
373
|
+
}): NativeWikiQueryResult {
|
|
374
|
+
const root = input.root ?? process.cwd();
|
|
375
|
+
ensureNativeWikiCurrent({ root, autoUpdate: input.autoUpdate });
|
|
376
|
+
const index = readNativeWikiIndex(root);
|
|
377
|
+
const graph = readNativeWikiGraphIfPresent(root);
|
|
378
|
+
const tokens = tokenizeQuery(input.query);
|
|
379
|
+
|
|
380
|
+
// Phase 1: token-match scoring
|
|
381
|
+
const candidates = index.files
|
|
382
|
+
.map((file) => {
|
|
383
|
+
const matches = nativeFileMatches(file, tokens);
|
|
384
|
+
return {
|
|
385
|
+
path: file.path,
|
|
386
|
+
source: "file-index" as const,
|
|
387
|
+
score: matches.reduce((sum, match) => sum + match.score, 0),
|
|
388
|
+
matches: matches.map((match) => match.label),
|
|
389
|
+
line: matches.find((m) => m.line)?.line,
|
|
390
|
+
};
|
|
391
|
+
})
|
|
392
|
+
.filter((result) => result.score > 0);
|
|
393
|
+
|
|
394
|
+
// Phase 2: PageRank re-ranking using matched files as seeds
|
|
395
|
+
if (graph && candidates.length > 0) {
|
|
396
|
+
const seeds = new Set(candidates.map((c) => c.path));
|
|
397
|
+
const pageRanks = computeNativePageRank(graph, seeds);
|
|
398
|
+
for (const candidate of candidates) {
|
|
399
|
+
const pr = pageRanks.get(candidate.path) ?? 0;
|
|
400
|
+
candidate.score = candidate.score * (1 + pr * 5);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const scored = candidates
|
|
405
|
+
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
406
|
+
.slice(0, input.limit ?? 5);
|
|
407
|
+
const contextFiles = scored.map((result) => result.path);
|
|
408
|
+
return {
|
|
409
|
+
provider: "gxpm",
|
|
410
|
+
query: input.query,
|
|
411
|
+
results: scored,
|
|
412
|
+
contextFiles,
|
|
413
|
+
suggestedDocs: suggestedNativeDocs(root, contextFiles, tokens),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function clipContextByTokenBudget(
|
|
418
|
+
root: string,
|
|
419
|
+
files: string[],
|
|
420
|
+
budgetTokens: number,
|
|
421
|
+
): string[] {
|
|
422
|
+
let used = 0;
|
|
423
|
+
const kept: string[] = [];
|
|
424
|
+
for (const path of files) {
|
|
425
|
+
const content = safeRead(join(root, path));
|
|
426
|
+
const estimated = Math.ceil(content.length / 4);
|
|
427
|
+
if (used + estimated > budgetTokens && kept.length > 0) {
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
kept.push(path);
|
|
431
|
+
used += estimated;
|
|
432
|
+
}
|
|
433
|
+
return kept;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function getNativeWikiContextForIssue(input: {
|
|
437
|
+
root?: string;
|
|
438
|
+
issueId: string;
|
|
439
|
+
phase?: GxpmPhase | string;
|
|
440
|
+
limit?: number;
|
|
441
|
+
autoUpdate?: boolean;
|
|
442
|
+
}): NativeWikiIssueContext {
|
|
443
|
+
const root = input.root ?? process.cwd();
|
|
444
|
+
ensureNativeWikiCurrent({ root, autoUpdate: input.autoUpdate });
|
|
445
|
+
const state = readIssueState({ root, issueId: input.issueId });
|
|
446
|
+
const phase = resolveIssueContextPhase(input.phase ?? state.currentPhase);
|
|
447
|
+
const artifacts = readIssueContextArtifacts(root, input.issueId);
|
|
448
|
+
const query = buildIssueContextQuery({
|
|
449
|
+
issueId: input.issueId,
|
|
450
|
+
issueType: state.issueType ?? "feature",
|
|
451
|
+
currentPhase: state.currentPhase,
|
|
452
|
+
phase,
|
|
453
|
+
artifacts,
|
|
454
|
+
});
|
|
455
|
+
const result = queryNativeWiki({ root, query, limit: input.limit });
|
|
456
|
+
const budget = parseInt(process.env.GXPM_WIKI_MAX_CONTEXT_TOKENS ?? "8192", 10);
|
|
457
|
+
const clippedFiles = clipContextByTokenBudget(root, result.contextFiles, budget);
|
|
458
|
+
return {
|
|
459
|
+
schemaVersion: 1,
|
|
460
|
+
provider: "gxpm",
|
|
461
|
+
issueId: input.issueId,
|
|
462
|
+
currentPhase: state.currentPhase,
|
|
463
|
+
phase,
|
|
464
|
+
query,
|
|
465
|
+
artifactsUsed: artifacts.map((artifact) => ({
|
|
466
|
+
type: artifact.type,
|
|
467
|
+
writtenAt: artifact.writtenAt,
|
|
468
|
+
})),
|
|
469
|
+
results: result.results.filter((r) => clippedFiles.includes(r.path)),
|
|
470
|
+
contextFiles: clippedFiles,
|
|
471
|
+
suggestedDocs: suggestedNativeDocs(root, clippedFiles, tokenizeQuery(query)),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function evaluateNativeWiki(input: {
|
|
476
|
+
root?: string;
|
|
477
|
+
now?: Date;
|
|
478
|
+
queryScenarios?: string[];
|
|
479
|
+
} = {}): NativeWikiEvalReport {
|
|
480
|
+
const root = input.root ?? process.cwd();
|
|
481
|
+
const now = input.now ?? new Date();
|
|
482
|
+
const status = getNativeWikiStatus({ root, now });
|
|
483
|
+
const index = readNativeWikiIndexIfPresent(root);
|
|
484
|
+
const graph = readNativeWikiGraphIfPresent(root);
|
|
485
|
+
const dimensions = readNativeWikiDimensionsIfPresent(root);
|
|
486
|
+
const clusters = index && dimensions ? buildNativeProjectTopicClusters(index, dimensions) : [];
|
|
487
|
+
const sourceCoverage = buildNativeWikiSourceCoverage(root, status.docs, index, graph, dimensions);
|
|
488
|
+
const queryScenarios = index
|
|
489
|
+
? (input.queryScenarios ?? DEFAULT_NATIVE_WIKI_EVAL_QUERIES).map((query) => {
|
|
490
|
+
const result = queryNativeWiki({ root, query, limit: 5 });
|
|
491
|
+
return {
|
|
492
|
+
query,
|
|
493
|
+
topFiles: result.contextFiles,
|
|
494
|
+
suggestedDocs: result.suggestedDocs,
|
|
495
|
+
};
|
|
496
|
+
})
|
|
497
|
+
: [];
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
schemaVersion: 1,
|
|
501
|
+
provider: "gxpm",
|
|
502
|
+
generatedAt: now.toISOString(),
|
|
503
|
+
native: {
|
|
504
|
+
status,
|
|
505
|
+
generatedDocs: {
|
|
506
|
+
count: status.docs.length,
|
|
507
|
+
names: status.docs.map((doc) => doc.replace(`${NATIVE_WIKI_CONTENT_ROOT}/`, "")),
|
|
508
|
+
},
|
|
509
|
+
projectTopics: {
|
|
510
|
+
clusterCount: clusters.length,
|
|
511
|
+
clusterTitles: clusters.map((cluster) => cluster.rule.title),
|
|
512
|
+
},
|
|
513
|
+
sourceCoverage,
|
|
514
|
+
queryScenarios,
|
|
515
|
+
},
|
|
516
|
+
recommendations: nativeWikiEvalRecommendations(status, sourceCoverage, clusters.length),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const ISSUE_CONTEXT_ARTIFACT_PRIORITY: ArtifactType[] = [
|
|
521
|
+
"issue-intake",
|
|
522
|
+
"acceptance-contract",
|
|
523
|
+
"triage-report",
|
|
524
|
+
"implementation-plan",
|
|
525
|
+
"dispatch-handoff",
|
|
526
|
+
"local-verify",
|
|
527
|
+
"acceptance-check",
|
|
528
|
+
"self-review",
|
|
529
|
+
"ship-readiness",
|
|
530
|
+
"pr-check",
|
|
531
|
+
"verify-findings",
|
|
532
|
+
"qa-findings",
|
|
533
|
+
"land-findings",
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
function resolveIssueContextPhase(value: GxpmPhase | string): GxpmPhase {
|
|
537
|
+
if (!isGxpmPhase(value)) {
|
|
538
|
+
throw new Error(`Invalid phase: ${value}`);
|
|
539
|
+
}
|
|
540
|
+
return value;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function readIssueContextArtifacts(root: string, issueId: string) {
|
|
544
|
+
const records = listIssueArtifactsIfPresent(root, issueId);
|
|
545
|
+
const available = new Map(records.map((record) => [record.type, record]));
|
|
546
|
+
return ISSUE_CONTEXT_ARTIFACT_PRIORITY.filter((type) => available.has(type)).map((type) => {
|
|
547
|
+
const stored = readArtifact({ root, issueId, type });
|
|
548
|
+
return {
|
|
549
|
+
type,
|
|
550
|
+
writtenAt: stored.writtenAt,
|
|
551
|
+
payload: stored.payload,
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function listIssueArtifactsIfPresent(root: string, issueId: string) {
|
|
557
|
+
try {
|
|
558
|
+
return listArtifacts({ root, issueId });
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (error instanceof Error && error.message === `Artifact index not found: ${issueId}`) {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function buildIssueContextQuery(input: {
|
|
568
|
+
issueId: string;
|
|
569
|
+
issueType: string;
|
|
570
|
+
currentPhase: GxpmPhase;
|
|
571
|
+
phase: GxpmPhase;
|
|
572
|
+
artifacts: Array<{ type: ArtifactType; payload: unknown }>;
|
|
573
|
+
}) {
|
|
574
|
+
const values = [
|
|
575
|
+
input.issueId,
|
|
576
|
+
input.issueType,
|
|
577
|
+
input.currentPhase,
|
|
578
|
+
input.phase,
|
|
579
|
+
...input.artifacts.flatMap((artifact) => [
|
|
580
|
+
artifact.type,
|
|
581
|
+
...payloadSearchText(artifact.payload),
|
|
582
|
+
]),
|
|
583
|
+
];
|
|
584
|
+
return values.join(" ").replace(/\s+/g, " ").trim().slice(0, 4000);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function payloadSearchText(payload: unknown) {
|
|
588
|
+
const values: string[] = [];
|
|
589
|
+
collectPayloadSearchText(payload, values, 0, { length: 0 });
|
|
590
|
+
return values;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function collectPayloadSearchText(
|
|
594
|
+
value: unknown,
|
|
595
|
+
values: string[],
|
|
596
|
+
depth: number,
|
|
597
|
+
state: { length: number },
|
|
598
|
+
) {
|
|
599
|
+
if (depth > 5 || state.length > 4000) return;
|
|
600
|
+
if (typeof value === "string") {
|
|
601
|
+
pushPayloadSearchText(values, state, value);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
605
|
+
pushPayloadSearchText(values, state, String(value));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (Array.isArray(value)) {
|
|
609
|
+
value.forEach((item) => collectPayloadSearchText(item, values, depth + 1, state));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (value && typeof value === "object") {
|
|
613
|
+
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
|
|
614
|
+
pushPayloadSearchText(values, state, key);
|
|
615
|
+
collectPayloadSearchText(nested, values, depth + 1, state);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function pushPayloadSearchText(values: string[], state: { length: number }, value: string) {
|
|
621
|
+
if (state.length > 4000) return;
|
|
622
|
+
values.push(value);
|
|
623
|
+
state.length += value.length + 1;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function buildNativeWikiSourceCoverage(
|
|
627
|
+
root: string,
|
|
628
|
+
docs: string[],
|
|
629
|
+
index: NativeWikiIndex | null,
|
|
630
|
+
graph: NativeWikiGraph | null,
|
|
631
|
+
dimensions: NativeWikiDimensions | null,
|
|
632
|
+
) {
|
|
633
|
+
const indexedFiles = index?.files.map((file) => file.path) ?? [];
|
|
634
|
+
const anchored = new Set<string>();
|
|
635
|
+
let docsWithSourceAnchors = 0;
|
|
636
|
+
for (const doc of docs) {
|
|
637
|
+
const cited = extractCitedFiles(safeRead(join(root, doc)));
|
|
638
|
+
if (cited.length > 0) docsWithSourceAnchors += 1;
|
|
639
|
+
for (const file of cited) anchored.add(file);
|
|
640
|
+
}
|
|
641
|
+
const orphanIndexedFiles = indexedFiles.filter((file) => !anchored.has(file)).sort();
|
|
642
|
+
return {
|
|
643
|
+
indexedFiles: indexedFiles.length,
|
|
644
|
+
dimensionedFiles: dimensions?.files.length ?? 0,
|
|
645
|
+
graphEdges: graph?.edges.length ?? 0,
|
|
646
|
+
docsWithSourceAnchors,
|
|
647
|
+
anchoredFiles: [...anchored].filter((file) => indexedFiles.includes(file)).length,
|
|
648
|
+
orphanIndexedFiles: orphanIndexedFiles.length,
|
|
649
|
+
orphanIndexedFileExamples: orphanIndexedFiles.slice(0, 20),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function nativeWikiEvalRecommendations(
|
|
654
|
+
status: NativeWikiStatus,
|
|
655
|
+
coverage: NativeWikiEvalReport["native"]["sourceCoverage"],
|
|
656
|
+
projectTopicClusterCount: number,
|
|
657
|
+
) {
|
|
658
|
+
const recommendations: string[] = [];
|
|
659
|
+
if (status.state === "absent") recommendations.push("Run gxpm wiki init.");
|
|
660
|
+
if (status.state === "stale") recommendations.push("Run gxpm wiki update.");
|
|
661
|
+
if (status.state === "current" && projectTopicClusterCount === 0) {
|
|
662
|
+
recommendations.push("Review native topic rules; no project topic clusters were inferred.");
|
|
663
|
+
}
|
|
664
|
+
if (status.state === "current" && coverage.orphanIndexedFiles > 0) {
|
|
665
|
+
recommendations.push("Use orphanIndexedFileExamples to choose the next topic coverage improvement.");
|
|
666
|
+
}
|
|
667
|
+
if (recommendations.length === 0) {
|
|
668
|
+
recommendations.push("Native wiki eval is current; use report metrics to choose the next wiki improvement.");
|
|
669
|
+
}
|
|
670
|
+
return recommendations;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function extractCitedFiles(markdown: string): string[] {
|
|
674
|
+
const files = new Set<string>();
|
|
675
|
+
const re = /file:\/\/([^)#\s]+)(?:#[^)]+)?/g;
|
|
676
|
+
let match: RegExpExecArray | null;
|
|
677
|
+
while ((match = re.exec(markdown)) !== null) {
|
|
678
|
+
files.add(decodeFileUrlPath(match[1]));
|
|
679
|
+
}
|
|
680
|
+
return [...files].sort();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function isOlderThanWeek(value: string | undefined, now: Date) {
|
|
684
|
+
if (!value) return true;
|
|
685
|
+
const time = Date.parse(value);
|
|
686
|
+
if (!Number.isFinite(time)) return true;
|
|
687
|
+
return now.getTime() - time >= WEEK_MS;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function isDescendant(candidate: string, parent: string) {
|
|
691
|
+
const childPath = relative(parent, candidate);
|
|
692
|
+
return childPath !== "" && childPath !== ".." && !childPath.startsWith(`..${sep}`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function isAfter(value: string | undefined, baseline: string | undefined) {
|
|
696
|
+
if (!value || !baseline) return false;
|
|
697
|
+
const valueTime = Date.parse(value);
|
|
698
|
+
const baselineTime = Date.parse(baseline);
|
|
699
|
+
return Number.isFinite(valueTime) && Number.isFinite(baselineTime) && valueTime > baselineTime;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function isDirectory(path: string) {
|
|
703
|
+
try {
|
|
704
|
+
return statSync(path).isDirectory();
|
|
705
|
+
} catch {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function sha256Hex(content: string): string {
|
|
711
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
712
|
+
hasher.update(content);
|
|
713
|
+
return hasher.digest("hex");
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function writeNativeWiki(input: {
|
|
717
|
+
root?: string;
|
|
718
|
+
now?: Date;
|
|
719
|
+
mode: NativeWikiBuildResult["mode"];
|
|
720
|
+
}): NativeWikiBuildResult {
|
|
721
|
+
const root = input.root ?? process.cwd();
|
|
722
|
+
const now = input.now ?? new Date();
|
|
723
|
+
const snapshot = buildNativeWikiIndexSnapshot({ root, now });
|
|
724
|
+
const index = snapshot.index;
|
|
725
|
+
const graph = buildNativeWikiGraph(index);
|
|
726
|
+
const dimensions = buildNativeWikiDimensions({ index, graph, contents: snapshot.contents });
|
|
727
|
+
const state: NativeWikiState = {
|
|
728
|
+
schemaVersion: 1,
|
|
729
|
+
provider: "gxpm",
|
|
730
|
+
status: "idle",
|
|
731
|
+
baseCommit: currentGitCommit(root),
|
|
732
|
+
generatedAt: now.toISOString(),
|
|
733
|
+
indexPath: NATIVE_WIKI_INDEX_PATH,
|
|
734
|
+
graphPath: NATIVE_WIKI_GRAPH_PATH,
|
|
735
|
+
dimensionsPath: NATIVE_WIKI_DIMENSIONS_PATH,
|
|
736
|
+
contentRoot: NATIVE_WIKI_CONTENT_ROOT,
|
|
737
|
+
queuedCommit: null,
|
|
738
|
+
};
|
|
739
|
+
mkdirSync(join(root, NATIVE_WIKI_ROOT, "index"), { recursive: true });
|
|
740
|
+
mkdirSync(join(root, NATIVE_WIKI_CONTENT_ROOT), { recursive: true });
|
|
741
|
+
writeJson(join(root, NATIVE_WIKI_INDEX_PATH), index);
|
|
742
|
+
writeJson(join(root, NATIVE_WIKI_GRAPH_PATH), graph);
|
|
743
|
+
writeJson(join(root, NATIVE_WIKI_DIMENSIONS_PATH), dimensions);
|
|
744
|
+
writeJson(join(root, NATIVE_WIKI_STATE_PATH), state);
|
|
745
|
+
const docs = writeNativeWikiDocs(root, state, index, graph, dimensions);
|
|
746
|
+
return { provider: "gxpm", mode: input.mode, state, index, graph, dimensions, docs };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function updateNativeWikiIncremental(input: { root?: string; now?: Date }): NativeWikiBuildResult {
|
|
750
|
+
const root = input.root ?? process.cwd();
|
|
751
|
+
const now = input.now ?? new Date();
|
|
752
|
+
|
|
753
|
+
// Read existing artifacts
|
|
754
|
+
const existingIndex = readNativeWikiIndex(root);
|
|
755
|
+
const existingGraph = readNativeWikiGraphIfPresent(root) ?? { schemaVersion: 1 as const, provider: "gxpm" as const, generatedAt: "", nodes: [], edges: [], unresolvedImports: [] };
|
|
756
|
+
const existingState = readNativeWikiStateIfPresent(root);
|
|
757
|
+
|
|
758
|
+
// Determine changed files via git diff against baseCommit
|
|
759
|
+
const changedFiles = getChangedFilesViaGitDiff(root, existingState?.baseCommit ?? null);
|
|
760
|
+
|
|
761
|
+
// Find downstream dependents (files that import changed files)
|
|
762
|
+
const dependentFiles = findDependents(existingGraph, changedFiles);
|
|
763
|
+
|
|
764
|
+
// Combine files that need re-processing
|
|
765
|
+
const filesToProcess = new Set([...changedFiles, ...dependentFiles]);
|
|
766
|
+
|
|
767
|
+
// Current tracked files on disk
|
|
768
|
+
const trackedPaths = listNativeRepoFiles(root).map((file) => toRepoPath(root, file));
|
|
769
|
+
const trackedSet = new Set(trackedPaths);
|
|
770
|
+
|
|
771
|
+
// Build updated index: keep unchanged files, re-parse changed/dependent/new
|
|
772
|
+
const contents = new Map<string, string>();
|
|
773
|
+
const updatedFiles: NativeWikiFileEntry[] = [];
|
|
774
|
+
const existingByPath = new Map(existingIndex.files.map((f) => [f.path, f]));
|
|
775
|
+
|
|
776
|
+
for (const file of existingIndex.files) {
|
|
777
|
+
if (!trackedSet.has(file.path)) {
|
|
778
|
+
// File was deleted — skip it
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (filesToProcess.has(file.path)) {
|
|
782
|
+
// Changed or dependent — re-parse
|
|
783
|
+
updatedFiles.push(summarizeNativeFile(root, join(root, file.path), contents));
|
|
784
|
+
} else {
|
|
785
|
+
// Unchanged — keep existing entry (including hash)
|
|
786
|
+
// Backfill symbols if missing from older index schema
|
|
787
|
+
updatedFiles.push({ ...file, symbols: file.symbols ?? [] });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Add newly created files
|
|
792
|
+
for (const path of trackedPaths) {
|
|
793
|
+
if (!existingByPath.has(path)) {
|
|
794
|
+
updatedFiles.push(summarizeNativeFile(root, join(root, path), contents));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
updatedFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
799
|
+
|
|
800
|
+
const index: NativeWikiIndex = {
|
|
801
|
+
schemaVersion: 1,
|
|
802
|
+
provider: "gxpm",
|
|
803
|
+
generatedAt: now.toISOString(),
|
|
804
|
+
files: updatedFiles,
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
const graph = buildNativeWikiGraph(index);
|
|
808
|
+
const dimensions = buildNativeWikiDimensions({ index, graph, contents });
|
|
809
|
+
|
|
810
|
+
const state: NativeWikiState = {
|
|
811
|
+
schemaVersion: 1,
|
|
812
|
+
provider: "gxpm",
|
|
813
|
+
status: "idle",
|
|
814
|
+
baseCommit: currentGitCommit(root),
|
|
815
|
+
generatedAt: now.toISOString(),
|
|
816
|
+
indexPath: NATIVE_WIKI_INDEX_PATH,
|
|
817
|
+
graphPath: NATIVE_WIKI_GRAPH_PATH,
|
|
818
|
+
dimensionsPath: NATIVE_WIKI_DIMENSIONS_PATH,
|
|
819
|
+
contentRoot: NATIVE_WIKI_CONTENT_ROOT,
|
|
820
|
+
queuedCommit: null,
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
mkdirSync(join(root, NATIVE_WIKI_ROOT, "index"), { recursive: true });
|
|
824
|
+
mkdirSync(join(root, NATIVE_WIKI_CONTENT_ROOT), { recursive: true });
|
|
825
|
+
writeJson(join(root, NATIVE_WIKI_INDEX_PATH), index);
|
|
826
|
+
writeJson(join(root, NATIVE_WIKI_GRAPH_PATH), graph);
|
|
827
|
+
writeJson(join(root, NATIVE_WIKI_DIMENSIONS_PATH), dimensions);
|
|
828
|
+
writeJson(join(root, NATIVE_WIKI_STATE_PATH), state);
|
|
829
|
+
const docs = writeNativeWikiDocs(root, state, index, graph, dimensions, changedFiles);
|
|
830
|
+
return { provider: "gxpm", mode: "update", state, index, graph, dimensions, docs };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function getChangedFilesViaGitDiff(root: string, baseCommit: string | null): string[] {
|
|
834
|
+
if (!baseCommit) {
|
|
835
|
+
// No base commit known — fallback: treat all tracked files as changed
|
|
836
|
+
return listNativeRepoFiles(root).map((file) => toRepoPath(root, file));
|
|
837
|
+
}
|
|
838
|
+
const result = Bun.spawnSync({
|
|
839
|
+
cmd: ["git", "diff", "--name-only", `${baseCommit}..HEAD`, "--"],
|
|
840
|
+
cwd: root,
|
|
841
|
+
stdout: "pipe",
|
|
842
|
+
stderr: "pipe",
|
|
843
|
+
});
|
|
844
|
+
if (result.exitCode !== 0) {
|
|
845
|
+
// Fallback to filesystem scan if git diff fails
|
|
846
|
+
return listNativeRepoFiles(root).map((file) => toRepoPath(root, file));
|
|
847
|
+
}
|
|
848
|
+
return result.stdout
|
|
849
|
+
.toString()
|
|
850
|
+
.split("\n")
|
|
851
|
+
.map((line) => line.trim())
|
|
852
|
+
.filter(Boolean);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function findDependents(graph: NativeWikiGraph, changedFiles: string[]): string[] {
|
|
856
|
+
const changedSet = new Set(changedFiles);
|
|
857
|
+
const dependents = new Set<string>();
|
|
858
|
+
for (const edge of graph.edges) {
|
|
859
|
+
if (edge.kind === "imports" && changedSet.has(edge.to)) {
|
|
860
|
+
dependents.add(edge.from);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return [...dependents];
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function buildNativeWikiGraph(index: NativeWikiIndex): NativeWikiGraph {
|
|
867
|
+
const filePaths = new Set(index.files.map((file) => file.path));
|
|
868
|
+
const edges: NativeWikiGraphEdge[] = [];
|
|
869
|
+
const unresolvedImports: NativeWikiGraph["unresolvedImports"] = [];
|
|
870
|
+
for (const file of index.files) {
|
|
871
|
+
for (const specifier of file.imports) {
|
|
872
|
+
const target = resolveNativeImport(file.path, specifier, filePaths);
|
|
873
|
+
if (target) {
|
|
874
|
+
edges.push({ from: file.path, to: target, kind: "imports" });
|
|
875
|
+
} else if (specifier.startsWith(".")) {
|
|
876
|
+
unresolvedImports.push({ from: file.path, specifier });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
schemaVersion: 1,
|
|
882
|
+
provider: "gxpm",
|
|
883
|
+
generatedAt: index.generatedAt,
|
|
884
|
+
nodes: index.files.map((file) => ({ path: file.path, language: file.language })),
|
|
885
|
+
edges: dedupeBy(edges, (edge) => `${edge.from}\0${edge.to}\0${edge.kind}`),
|
|
886
|
+
unresolvedImports,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function computeNativePageRank(
|
|
891
|
+
graph: NativeWikiGraph,
|
|
892
|
+
seeds?: Set<string>,
|
|
893
|
+
options: { damping?: number; iterations?: number; epsilon?: number } = {},
|
|
894
|
+
): Map<string, number> {
|
|
895
|
+
const { damping = 0.85, iterations = 20, epsilon = 1e-6 } = options;
|
|
896
|
+
const nodePaths = graph.nodes.map((n) => n.path);
|
|
897
|
+
const n = nodePaths.length;
|
|
898
|
+
if (n === 0) return new Map();
|
|
899
|
+
|
|
900
|
+
// Build adjacency list (outgoing edges)
|
|
901
|
+
const outgoing = new Map<string, string[]>();
|
|
902
|
+
for (const path of nodePaths) outgoing.set(path, []);
|
|
903
|
+
for (const edge of graph.edges) {
|
|
904
|
+
if (edge.kind === "imports") {
|
|
905
|
+
outgoing.get(edge.from)?.push(edge.to);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Normalize outgoing counts (teleport for dangling nodes)
|
|
910
|
+
const outCounts = new Map<string, number>();
|
|
911
|
+
for (const path of nodePaths) {
|
|
912
|
+
const outs = outgoing.get(path) ?? [];
|
|
913
|
+
outCounts.set(path, outs.length > 0 ? outs.length : n);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Initial rank: uniform, or boosted for seeds
|
|
917
|
+
const ranks = new Map<string, number>();
|
|
918
|
+
const base = 1 / n;
|
|
919
|
+
for (const path of nodePaths) {
|
|
920
|
+
ranks.set(path, seeds?.has(path) ? base * 3 : base);
|
|
921
|
+
}
|
|
922
|
+
normalizeMap(ranks);
|
|
923
|
+
|
|
924
|
+
// Personalization vector: uniform, or boosted for seeds
|
|
925
|
+
const personal = new Map<string, number>();
|
|
926
|
+
for (const path of nodePaths) {
|
|
927
|
+
personal.set(path, seeds?.has(path) ? base * 3 : base);
|
|
928
|
+
}
|
|
929
|
+
normalizeMap(personal);
|
|
930
|
+
|
|
931
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
932
|
+
const newRanks = new Map<string, number>();
|
|
933
|
+
for (const path of nodePaths) {
|
|
934
|
+
let sum = 0;
|
|
935
|
+
for (const edge of graph.edges) {
|
|
936
|
+
if (edge.to === path && edge.kind === "imports") {
|
|
937
|
+
const outCount = outCounts.get(edge.from) ?? n;
|
|
938
|
+
sum += (ranks.get(edge.from) ?? 0) / outCount;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
// Dangling node: distribute rank uniformly
|
|
942
|
+
const outs = outgoing.get(path) ?? [];
|
|
943
|
+
if (outs.length === 0) {
|
|
944
|
+
sum += (ranks.get(path) ?? 0) / n;
|
|
945
|
+
}
|
|
946
|
+
newRanks.set(path, (1 - damping) * (personal.get(path) ?? base) + damping * sum);
|
|
947
|
+
}
|
|
948
|
+
normalizeMap(newRanks);
|
|
949
|
+
|
|
950
|
+
// Check convergence
|
|
951
|
+
let diff = 0;
|
|
952
|
+
for (const path of nodePaths) {
|
|
953
|
+
diff += Math.abs((newRanks.get(path) ?? 0) - (ranks.get(path) ?? 0));
|
|
954
|
+
}
|
|
955
|
+
for (const path of nodePaths) ranks.set(path, newRanks.get(path) ?? 0);
|
|
956
|
+
if (diff < epsilon) break;
|
|
957
|
+
}
|
|
958
|
+
return ranks;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function normalizeMap(map: Map<string, number>): void {
|
|
962
|
+
let sum = 0;
|
|
963
|
+
for (const v of map.values()) sum += v;
|
|
964
|
+
if (sum === 0) return;
|
|
965
|
+
for (const key of map.keys()) {
|
|
966
|
+
map.set(key, (map.get(key) ?? 0) / sum);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function buildNativeWikiDimensions(input: {
|
|
971
|
+
index: NativeWikiIndex;
|
|
972
|
+
graph: NativeWikiGraph;
|
|
973
|
+
contents: Map<string, string>;
|
|
974
|
+
}): NativeWikiDimensions {
|
|
975
|
+
const outgoing = new Map<string, NativeWikiGraphEdge[]>();
|
|
976
|
+
const incoming = new Map<string, NativeWikiGraphEdge[]>();
|
|
977
|
+
for (const edge of input.graph.edges) {
|
|
978
|
+
pushMapValue(outgoing, edge.from, edge);
|
|
979
|
+
pushMapValue(incoming, edge.to, edge);
|
|
980
|
+
}
|
|
981
|
+
const unresolved = new Map<string, string[]>();
|
|
982
|
+
for (const entry of input.graph.unresolvedImports) {
|
|
983
|
+
pushMapValue(unresolved, entry.from, entry.specifier);
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
schemaVersion: 1,
|
|
987
|
+
provider: "gxpm",
|
|
988
|
+
generatedAt: input.index.generatedAt,
|
|
989
|
+
files: input.index.files.map((file) =>
|
|
990
|
+
buildNativeFileDimensions({
|
|
991
|
+
file,
|
|
992
|
+
content: input.contents.get(file.path) ?? "",
|
|
993
|
+
outgoing: outgoing.get(file.path) ?? [],
|
|
994
|
+
incoming: incoming.get(file.path) ?? [],
|
|
995
|
+
unresolvedImports: unresolved.get(file.path) ?? [],
|
|
996
|
+
}),
|
|
997
|
+
),
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function buildNativeFileDimensions(input: {
|
|
1002
|
+
file: NativeWikiFileEntry;
|
|
1003
|
+
content: string;
|
|
1004
|
+
outgoing: NativeWikiGraphEdge[];
|
|
1005
|
+
incoming: NativeWikiGraphEdge[];
|
|
1006
|
+
unresolvedImports: string[];
|
|
1007
|
+
}): NativeWikiFileDimensions {
|
|
1008
|
+
return {
|
|
1009
|
+
path: input.file.path,
|
|
1010
|
+
language: input.file.language,
|
|
1011
|
+
dimensions: {
|
|
1012
|
+
structure: nativeStructureSignals(input.file),
|
|
1013
|
+
symbols: nativeSymbolSignals(input.file),
|
|
1014
|
+
apis: nativeApiSignals(input.file, input.content),
|
|
1015
|
+
workflows: nativeWorkflowSignals(input.file, input.content),
|
|
1016
|
+
config: nativeConfigSignals(input.file, input.content),
|
|
1017
|
+
tests: nativeTestSignals(input.file, input.content),
|
|
1018
|
+
docs: nativeDocSignals(input.file),
|
|
1019
|
+
relations: nativeRelationSignals(input.file, input.outgoing, input.incoming, input.unresolvedImports),
|
|
1020
|
+
},
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function nativeStructureSignals(file: NativeWikiFileEntry) {
|
|
1025
|
+
const signals = new Set<string>([`language:${file.language}`]);
|
|
1026
|
+
const parts = file.path.split("/");
|
|
1027
|
+
if (parts.length > 1) signals.add(`root:${parts[0]}`);
|
|
1028
|
+
const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
|
|
1029
|
+
signals.add(`dir:${dir}`);
|
|
1030
|
+
const ext = extname(file.path).toLowerCase();
|
|
1031
|
+
if (ext) signals.add(`ext:${ext}`);
|
|
1032
|
+
return sortedSignals(signals);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function nativeSymbolSignals(file: NativeWikiFileEntry) {
|
|
1036
|
+
const signals = new Set<string>();
|
|
1037
|
+
for (const name of file.exports) signals.add(`export:${name}`);
|
|
1038
|
+
return sortedSignals(signals);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function nativeApiSignals(file: NativeWikiFileEntry, content: string) {
|
|
1042
|
+
const signals = new Set<string>();
|
|
1043
|
+
if (file.path === "bin/gxpm" || file.path === "scripts/gxpm.ts") signals.add("cli:gxpm");
|
|
1044
|
+
collectRegex(content, /\bgxpm(?:\s+[a-z][\w-]*){1,3}/g, signals, undefined, "cli:");
|
|
1045
|
+
collectRegex(content, /\b(?:GET|POST|PUT|PATCH|DELETE)\s+["'`]([^"'`]+)["'`]/g, signals, undefined, "http:");
|
|
1046
|
+
collectRegex(content, /\.(?:get|post|put|patch|delete)\(\s*["'`]([^"'`]+)["'`]/g, signals, undefined, "http:");
|
|
1047
|
+
return sortedSignals(signals);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function nativeWorkflowSignals(file: NativeWikiFileEntry, content: string) {
|
|
1051
|
+
const signals = new Set<string>();
|
|
1052
|
+
const path = file.path.toLowerCase();
|
|
1053
|
+
const workflowTokens = ["phase", "gate", "hook", "workflow", "transition", "triage", "dispatch", "verify", "qa", "land"];
|
|
1054
|
+
for (const token of workflowTokens) {
|
|
1055
|
+
if (path.includes(token)) signals.add(`path:${token}`);
|
|
1056
|
+
}
|
|
1057
|
+
if (path.startsWith(".githooks/") || path.includes("/hooks/")) signals.add("path:hook");
|
|
1058
|
+
for (const token of workflowTokens) {
|
|
1059
|
+
if (new RegExp(`\\b${token}\\b`, "i").test(content)) signals.add(`content:${token}`);
|
|
1060
|
+
}
|
|
1061
|
+
return sortedSignals(signals);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function nativeConfigSignals(file: NativeWikiFileEntry, content: string) {
|
|
1065
|
+
const signals = new Set<string>();
|
|
1066
|
+
const path = file.path.toLowerCase();
|
|
1067
|
+
if (path.includes("config")) signals.add("path:config");
|
|
1068
|
+
if (["json", "toml", "yaml"].includes(file.language)) signals.add(`format:${file.language}`);
|
|
1069
|
+
collectRegex(content, /\bprocess\.env\.([A-Z0-9_]+)/g, signals, undefined, "env:");
|
|
1070
|
+
collectRegex(content, /\b([A-Z][A-Z0-9_]{2,})\b/g, signals, undefined, "constant:");
|
|
1071
|
+
return sortedSignals(signals);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function nativeTestSignals(file: NativeWikiFileEntry, content: string) {
|
|
1075
|
+
const signals = new Set<string>();
|
|
1076
|
+
if (file.path.startsWith("test/") || /\.test\.[jt]sx?$/.test(file.path)) signals.add("path:test");
|
|
1077
|
+
if (/\bdescribe\s*\(/.test(content)) signals.add("runner:describe");
|
|
1078
|
+
if (/\btest\s*\(/.test(content)) signals.add("runner:test");
|
|
1079
|
+
return sortedSignals(signals);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function nativeDocSignals(file: NativeWikiFileEntry) {
|
|
1083
|
+
const signals = new Set<string>();
|
|
1084
|
+
const path = file.path.toLowerCase();
|
|
1085
|
+
if (path === "readme.md") signals.add("path:readme");
|
|
1086
|
+
if (path.startsWith("docs/")) signals.add("path:docs");
|
|
1087
|
+
if (file.language === "markdown") signals.add("format:markdown");
|
|
1088
|
+
for (const heading of file.headings) signals.add(`heading:${heading}`);
|
|
1089
|
+
return sortedSignals(signals);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function nativeRelationSignals(
|
|
1093
|
+
file: NativeWikiFileEntry,
|
|
1094
|
+
outgoing: NativeWikiGraphEdge[],
|
|
1095
|
+
incoming: NativeWikiGraphEdge[],
|
|
1096
|
+
unresolvedImports: string[],
|
|
1097
|
+
) {
|
|
1098
|
+
const signals = new Set<string>();
|
|
1099
|
+
for (const edge of outgoing) signals.add(`imports:${edge.to}`);
|
|
1100
|
+
for (const edge of incoming) signals.add(`imported-by:${edge.from}`);
|
|
1101
|
+
for (const specifier of unresolvedImports) signals.add(`unresolved:${specifier}`);
|
|
1102
|
+
for (const specifier of file.imports.filter((specifier) => !specifier.startsWith("."))) {
|
|
1103
|
+
signals.add(`external:${specifier}`);
|
|
1104
|
+
}
|
|
1105
|
+
return sortedSignals(signals);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function sortedSignals(values: Set<string>) {
|
|
1109
|
+
return [...values].sort();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function listNativeRepoFiles(root: string) {
|
|
1113
|
+
const files = gitTrackedRepoFiles(root) ?? fallbackNativeRepoFiles(root);
|
|
1114
|
+
return files
|
|
1115
|
+
.filter((file) => {
|
|
1116
|
+
if (isNativeSkippedRepoPath(toRepoPath(root, file))) return false;
|
|
1117
|
+
const ext = extname(file).toLowerCase();
|
|
1118
|
+
if (!NATIVE_TEXT_EXTENSIONS.has(ext)) return false;
|
|
1119
|
+
try {
|
|
1120
|
+
if (statSync(file).size > NATIVE_MAX_FILE_BYTES) return false;
|
|
1121
|
+
} catch {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
if (ext === "" && !isLikelyTextFile(file)) return false;
|
|
1125
|
+
return true;
|
|
1126
|
+
})
|
|
1127
|
+
.sort((a, b) => toRepoPath(root, a).localeCompare(toRepoPath(root, b)));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function isNativeSkippedRepoPath(repoPath: string) {
|
|
1131
|
+
const firstSegment = normalizeRepoPath(repoPath).split("/")[0];
|
|
1132
|
+
return NATIVE_SKIP_DIRS.has(firstSegment);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function fallbackNativeRepoFiles(root: string) {
|
|
1136
|
+
const files: string[] = [];
|
|
1137
|
+
walkNativeRepoFiles(root, (file) => files.push(file));
|
|
1138
|
+
return files;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function gitTrackedRepoFiles(root: string) {
|
|
1142
|
+
const result = Bun.spawnSync({
|
|
1143
|
+
cmd: ["git", "ls-files", "-z"],
|
|
1144
|
+
cwd: root,
|
|
1145
|
+
stdout: "pipe",
|
|
1146
|
+
stderr: "pipe",
|
|
1147
|
+
});
|
|
1148
|
+
if (result.exitCode !== 0) return null;
|
|
1149
|
+
return result.stdout
|
|
1150
|
+
.toString()
|
|
1151
|
+
.split("\0")
|
|
1152
|
+
.filter(Boolean)
|
|
1153
|
+
.map((path) => join(root, path))
|
|
1154
|
+
.filter((path) => {
|
|
1155
|
+
try {
|
|
1156
|
+
return statSync(path).isFile();
|
|
1157
|
+
} catch {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function summarizeNativeFile(root: string, file: string, contents?: Map<string, string>): NativeWikiFileEntry {
|
|
1164
|
+
const content = safeRead(file);
|
|
1165
|
+
const stat = statSync(file);
|
|
1166
|
+
const repoPath = toRepoPath(root, file);
|
|
1167
|
+
contents?.set(repoPath, content);
|
|
1168
|
+
return {
|
|
1169
|
+
path: repoPath,
|
|
1170
|
+
language: languageForPath(repoPath),
|
|
1171
|
+
sizeBytes: stat.size,
|
|
1172
|
+
mtimeMs: stat.mtimeMs,
|
|
1173
|
+
lineCount: countLines(content),
|
|
1174
|
+
exports: extractExports(content),
|
|
1175
|
+
imports: extractImports(content),
|
|
1176
|
+
headings: extractMarkdownHeadings(content),
|
|
1177
|
+
symbols: extractNativeSymbols(repoPath, content),
|
|
1178
|
+
contentHash: sha256Hex(content),
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function readWikiConfig(root: string): NativeWikiConfig {
|
|
1183
|
+
const path = join(root, NATIVE_WIKI_CONFIG_PATH);
|
|
1184
|
+
if (!existsSync(path)) return {};
|
|
1185
|
+
try {
|
|
1186
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
1187
|
+
if (!raw || typeof raw !== "object") return {};
|
|
1188
|
+
const candidate = raw as Record<string, unknown>;
|
|
1189
|
+
const config: NativeWikiConfig = {};
|
|
1190
|
+
if (Array.isArray(candidate.repo_notes)) {
|
|
1191
|
+
config.repo_notes = candidate.repo_notes.filter((v): v is string => typeof v === "string");
|
|
1192
|
+
}
|
|
1193
|
+
if (Array.isArray(candidate.priority_dirs)) {
|
|
1194
|
+
config.priority_dirs = candidate.priority_dirs.filter((v): v is string => typeof v === "string");
|
|
1195
|
+
}
|
|
1196
|
+
if (Array.isArray(candidate.exclude_from_map)) {
|
|
1197
|
+
config.exclude_from_map = candidate.exclude_from_map.filter((v): v is string => typeof v === "string");
|
|
1198
|
+
}
|
|
1199
|
+
return config;
|
|
1200
|
+
} catch {
|
|
1201
|
+
return {};
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function ensureWikiConfig(root: string): void {
|
|
1206
|
+
const path = join(root, NATIVE_WIKI_CONFIG_PATH);
|
|
1207
|
+
if (existsSync(path)) return;
|
|
1208
|
+
const template = {
|
|
1209
|
+
_comment: "gxpm native wiki configuration. Edit to customize wiki generation.",
|
|
1210
|
+
repo_notes: ["Add domain context notes here — they appear in Overview.md"],
|
|
1211
|
+
priority_dirs: ["core", "scripts"],
|
|
1212
|
+
exclude_from_map: ["tmp", "node_modules", "dist"],
|
|
1213
|
+
};
|
|
1214
|
+
writeFileSync(path, JSON.stringify(template, null, 2) + "\n", "utf8");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function writeNativeWikiDocs(
|
|
1218
|
+
root: string,
|
|
1219
|
+
state: NativeWikiState,
|
|
1220
|
+
index: NativeWikiIndex,
|
|
1221
|
+
graph: NativeWikiGraph,
|
|
1222
|
+
dimensions: NativeWikiDimensions,
|
|
1223
|
+
changedFiles?: string[],
|
|
1224
|
+
) {
|
|
1225
|
+
ensureWikiConfig(root);
|
|
1226
|
+
const config = readWikiConfig(root);
|
|
1227
|
+
const previousGeneratedPaths = readNativeWikiDocManifest(root);
|
|
1228
|
+
const projectTopicClusters = buildNativeProjectTopicClusters(index, dimensions, config.priority_dirs);
|
|
1229
|
+
const changedSet = changedFiles ? new Set(changedFiles) : null;
|
|
1230
|
+
|
|
1231
|
+
const excludeSet = new Set(config.exclude_from_map ?? []);
|
|
1232
|
+
|
|
1233
|
+
const docEntries: Array<readonly [string, () => string]> = [
|
|
1234
|
+
["Overview.md", () => renderNativeOverview(state, index, graph, config)],
|
|
1235
|
+
["File-Index.md", () => renderNativeFileIndex(index)],
|
|
1236
|
+
["Import-Map.md", () => renderNativeImportMap(graph)],
|
|
1237
|
+
["Project-Topics.md", () => renderNativeProjectTopics(index, graph, dimensions, projectTopicClusters)],
|
|
1238
|
+
["Architecture-Diagram.md", () => renderNativeArchitectureDiagram(graph)],
|
|
1239
|
+
...projectTopicClusters.map(
|
|
1240
|
+
(cluster) => [nativeProjectTopicFileName(cluster.rule), () => renderNativeProjectTopicPage(cluster, graph)] as const,
|
|
1241
|
+
),
|
|
1242
|
+
...NATIVE_WIKI_TOPICS.map((topic) => [topic.fileName, () => renderNativeTopicDoc(topic, index, graph)] as const),
|
|
1243
|
+
].filter(([name]) => {
|
|
1244
|
+
for (const pattern of excludeSet) {
|
|
1245
|
+
if (name === pattern || name.startsWith(pattern.replace(/\/$/, "") + "/")) return false;
|
|
1246
|
+
}
|
|
1247
|
+
return true;
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
const docs: Array<readonly [string, string]> = docEntries.map(([name, renderer]) => {
|
|
1251
|
+
if (changedSet && !isDocAffected(name, changedSet, projectTopicClusters)) {
|
|
1252
|
+
const existingPath = join(root, NATIVE_WIKI_CONTENT_ROOT, name);
|
|
1253
|
+
const existing = safeRead(existingPath);
|
|
1254
|
+
if (existing) return [name, existing] as const;
|
|
1255
|
+
}
|
|
1256
|
+
return [name, renderer()] as const;
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
const paths: string[] = [];
|
|
1260
|
+
for (const [name, content] of docs) {
|
|
1261
|
+
const path = join(root, NATIVE_WIKI_CONTENT_ROOT, name);
|
|
1262
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1263
|
+
writeFileSync(path, content);
|
|
1264
|
+
paths.push(toRepoPath(root, path));
|
|
1265
|
+
}
|
|
1266
|
+
writeModuleTreeJson(root, state, index);
|
|
1267
|
+
pruneStaleNativeWikiDocs(root, previousGeneratedPaths, paths);
|
|
1268
|
+
writeNativeWikiDocManifest(root, state, index, graph, dimensions, paths);
|
|
1269
|
+
return paths;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function isDocAffected(
|
|
1273
|
+
docName: string,
|
|
1274
|
+
changedSet: Set<string>,
|
|
1275
|
+
projectTopicClusters: NativeWikiProjectTopicCluster[],
|
|
1276
|
+
): boolean {
|
|
1277
|
+
// Global docs are always affected
|
|
1278
|
+
if (["Overview.md", "File-Index.md", "Import-Map.md", "Project-Topics.md"].includes(docName)) {
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Project topic pages: affected if any matched file changed
|
|
1283
|
+
const topicCluster = projectTopicClusters.find((c) => nativeProjectTopicFileName(c.rule) === docName);
|
|
1284
|
+
if (topicCluster) {
|
|
1285
|
+
return topicCluster.files.some((entry) => changedSet.has(entry.file.path));
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Fixed topic docs: affected if sourcePaths or sourcePrefixes intersect changed files
|
|
1289
|
+
const fixedTopic = NATIVE_WIKI_TOPICS.find((t) => t.fileName === docName);
|
|
1290
|
+
if (fixedTopic) {
|
|
1291
|
+
return changedSetHasTopicMatch(changedSet, fixedTopic);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function changedSetHasTopicMatch(changedSet: Set<string>, topic: NativeWikiTopic): boolean {
|
|
1298
|
+
for (const path of changedSet) {
|
|
1299
|
+
if (topic.sourcePaths.includes(path)) return true;
|
|
1300
|
+
if (topic.sourcePrefixes?.some((prefix) => path.startsWith(prefix))) return true;
|
|
1301
|
+
}
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function readNativeWikiDocManifest(root: string) {
|
|
1306
|
+
const path = join(root, NATIVE_WIKI_DOC_MANIFEST_PATH);
|
|
1307
|
+
if (!existsSync(path)) return [];
|
|
1308
|
+
try {
|
|
1309
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as { docs?: unknown };
|
|
1310
|
+
return Array.isArray(value.docs) ? value.docs.filter((doc): doc is string => typeof doc === "string") : [];
|
|
1311
|
+
} catch {
|
|
1312
|
+
return [];
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function writeModuleTreeJson(root: string, state: NativeWikiState, index: NativeWikiIndex) {
|
|
1317
|
+
const rootNode: NativeWikiModuleTreeNode = { name: ".", type: "directory", children: [], fileCount: 0 };
|
|
1318
|
+
for (const file of index.files) {
|
|
1319
|
+
const parts = file.path.split("/");
|
|
1320
|
+
let current = rootNode;
|
|
1321
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1322
|
+
const part = parts[i];
|
|
1323
|
+
if (i === parts.length - 1) {
|
|
1324
|
+
current.children = current.children ?? [];
|
|
1325
|
+
current.children.push({ name: part, type: "file" });
|
|
1326
|
+
current.fileCount = (current.fileCount ?? 0) + 1;
|
|
1327
|
+
} else {
|
|
1328
|
+
current.children = current.children ?? [];
|
|
1329
|
+
let next = current.children.find((c) => c.name === part && c.type === "directory");
|
|
1330
|
+
if (!next) {
|
|
1331
|
+
next = { name: part, type: "directory", children: [], fileCount: 0 };
|
|
1332
|
+
current.children.push(next);
|
|
1333
|
+
}
|
|
1334
|
+
current.fileCount = (current.fileCount ?? 0) + 1;
|
|
1335
|
+
current = next;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const tree: NativeWikiModuleTree = {
|
|
1340
|
+
schemaVersion: 1,
|
|
1341
|
+
provider: "gxpm",
|
|
1342
|
+
generatedAt: state.generatedAt,
|
|
1343
|
+
root: rootNode,
|
|
1344
|
+
};
|
|
1345
|
+
writeJson(join(root, NATIVE_WIKI_MODULE_TREE_PATH), tree);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function writeNativeWikiDocManifest(
|
|
1349
|
+
root: string,
|
|
1350
|
+
state: NativeWikiState,
|
|
1351
|
+
index: NativeWikiIndex,
|
|
1352
|
+
graph: NativeWikiGraph,
|
|
1353
|
+
dimensions: NativeWikiDimensions,
|
|
1354
|
+
generatedDocPaths: string[],
|
|
1355
|
+
) {
|
|
1356
|
+
writeJson(join(root, NATIVE_WIKI_DOC_MANIFEST_PATH), {
|
|
1357
|
+
schemaVersion: 1,
|
|
1358
|
+
provider: "gxpm",
|
|
1359
|
+
generatedAt: state.generatedAt,
|
|
1360
|
+
indexedFiles: index.files.length,
|
|
1361
|
+
graphEdges: graph.edges.length,
|
|
1362
|
+
dimensionedFiles: dimensions.files.length,
|
|
1363
|
+
docs: generatedDocPaths,
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function pruneStaleNativeWikiDocs(root: string, previousGeneratedPaths: string[], generatedDocPaths: string[]) {
|
|
1368
|
+
const previousGenerated = new Set(previousGeneratedPaths);
|
|
1369
|
+
const generated = new Set(generatedDocPaths);
|
|
1370
|
+
walkFiles(join(root, NATIVE_WIKI_CONTENT_ROOT), (file) => {
|
|
1371
|
+
if (!file.endsWith(".md")) return;
|
|
1372
|
+
const repoPath = toRepoPath(root, file);
|
|
1373
|
+
if (previousGenerated.has(repoPath) && !generated.has(repoPath)) rmSync(file, { force: true });
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function renderNativeOverview(state: NativeWikiState, index: NativeWikiIndex, graph: NativeWikiGraph, config?: NativeWikiConfig) {
|
|
1378
|
+
const languages = countBy(index.files, (file) => file.language)
|
|
1379
|
+
.map(([language, count]) => `- ${language}: ${count}`)
|
|
1380
|
+
.join("\n");
|
|
1381
|
+
const highSignalFiles = index.files
|
|
1382
|
+
.filter((file) => file.exports.length > 0 || file.headings.length > 0)
|
|
1383
|
+
.slice(0, 20)
|
|
1384
|
+
.map((file) => `- [${file.path}](${nativeFileUrl(file.path)})`)
|
|
1385
|
+
.join("\n");
|
|
1386
|
+
const repoNotesSection =
|
|
1387
|
+
config?.repo_notes && config.repo_notes.length > 0
|
|
1388
|
+
? ["## Repo Notes", "", ...config.repo_notes.map((note) => `> ${note.replace(/\n/g, "\n> ")}`), ""]
|
|
1389
|
+
: [];
|
|
1390
|
+
return [
|
|
1391
|
+
"# GXPM Wiki Overview",
|
|
1392
|
+
"",
|
|
1393
|
+
`Generated: ${state.generatedAt}`,
|
|
1394
|
+
`Base commit: ${state.baseCommit ?? "unknown"}`,
|
|
1395
|
+
`Indexed files: ${index.files.length}`,
|
|
1396
|
+
`Import edges: ${graph.edges.length}`,
|
|
1397
|
+
"",
|
|
1398
|
+
...repoNotesSection,
|
|
1399
|
+
"## Languages",
|
|
1400
|
+
"",
|
|
1401
|
+
languages || "- none",
|
|
1402
|
+
"",
|
|
1403
|
+
"## Navigation",
|
|
1404
|
+
"",
|
|
1405
|
+
"- [Project Topics](file://.gxpm/wiki/content/Project-Topics.md)",
|
|
1406
|
+
"- [File Index](file://.gxpm/wiki/content/File-Index.md)",
|
|
1407
|
+
"- [Import Map](file://.gxpm/wiki/content/Import-Map.md)",
|
|
1408
|
+
"- [Architecture Diagram](file://.gxpm/wiki/content/Architecture-Diagram.md)",
|
|
1409
|
+
"",
|
|
1410
|
+
"## High Signal Files",
|
|
1411
|
+
"",
|
|
1412
|
+
highSignalFiles || "- none",
|
|
1413
|
+
"",
|
|
1414
|
+
].join("\n");
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function renderNativeFileIndex(index: NativeWikiIndex) {
|
|
1418
|
+
const rows = index.files
|
|
1419
|
+
.slice(0, 200)
|
|
1420
|
+
.map((file) => `| [${file.path}](${nativeFileUrl(file.path, 1)}) | ${file.language} | ${file.exports.join(", ")} |`);
|
|
1421
|
+
return [
|
|
1422
|
+
"# File Index",
|
|
1423
|
+
"",
|
|
1424
|
+
"| File | Language | Exports |",
|
|
1425
|
+
"| --- | --- | --- |",
|
|
1426
|
+
...rows,
|
|
1427
|
+
"",
|
|
1428
|
+
].join("\n");
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function renderNativeImportMap(graph: NativeWikiGraph) {
|
|
1432
|
+
const edges = graph.edges
|
|
1433
|
+
.slice(0, 200)
|
|
1434
|
+
.map((edge) => `- [${edge.from}](${nativeFileUrl(edge.from, 1)}) -> [${edge.to}](${nativeFileUrl(edge.to, 1)})`);
|
|
1435
|
+
return [
|
|
1436
|
+
"# Import Map",
|
|
1437
|
+
"",
|
|
1438
|
+
"Lightweight import edges for human orientation. Use GitNexus for agent code intelligence, call chains, impact analysis, and refactoring decisions.",
|
|
1439
|
+
"",
|
|
1440
|
+
...edges,
|
|
1441
|
+
"",
|
|
1442
|
+
].join("\n");
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function renderNativeArchitectureDiagram(graph: NativeWikiGraph) {
|
|
1446
|
+
const fileSet = new Set(graph.nodes.map((n) => n.path));
|
|
1447
|
+
const mermaidEdges = graph.edges
|
|
1448
|
+
.filter((edge) => fileSet.has(edge.from) && fileSet.has(edge.to))
|
|
1449
|
+
.slice(0, 150)
|
|
1450
|
+
.map((edge) => {
|
|
1451
|
+
const from = edge.from.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1452
|
+
const to = edge.to.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1453
|
+
return ` ${from}["${escapeMarkdownCell(edge.from)}"] --> ${to}["${escapeMarkdownCell(edge.to)}"]`;
|
|
1454
|
+
});
|
|
1455
|
+
return [
|
|
1456
|
+
"# Architecture Diagram",
|
|
1457
|
+
"",
|
|
1458
|
+
"```mermaid",
|
|
1459
|
+
"flowchart LR",
|
|
1460
|
+
...mermaidEdges,
|
|
1461
|
+
"```",
|
|
1462
|
+
"",
|
|
1463
|
+
].join("\n");
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
interface NativeWikiTopic {
|
|
1467
|
+
fileName: string;
|
|
1468
|
+
title: string;
|
|
1469
|
+
summary: string;
|
|
1470
|
+
keywords: string[];
|
|
1471
|
+
sourcePaths: string[];
|
|
1472
|
+
sourcePrefixes?: string[];
|
|
1473
|
+
mermaid?: string;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
type NativeWikiDimensionKey = keyof NativeWikiFileDimensions["dimensions"];
|
|
1477
|
+
|
|
1478
|
+
interface NativeWikiProjectTopicRule {
|
|
1479
|
+
title: string;
|
|
1480
|
+
summary: string;
|
|
1481
|
+
hints: string[];
|
|
1482
|
+
dimensionKeys?: NativeWikiDimensionKey[];
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
interface NativeWikiProjectTopicFile {
|
|
1486
|
+
file: NativeWikiFileEntry;
|
|
1487
|
+
score: number;
|
|
1488
|
+
signals: string[];
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
interface NativeWikiProjectTopicCluster {
|
|
1492
|
+
rule: NativeWikiProjectTopicRule;
|
|
1493
|
+
files: NativeWikiProjectTopicFile[];
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const NATIVE_WIKI_PROJECT_TOPIC_FILE = "Project-Topics.md";
|
|
1497
|
+
|
|
1498
|
+
const NATIVE_WIKI_PROJECT_TOPIC_RULES: NativeWikiProjectTopicRule[] = [
|
|
1499
|
+
{
|
|
1500
|
+
title: "Phase And Gate System",
|
|
1501
|
+
summary: "Issue lifecycle, phase gates, transitions, and phase-specific artifacts inferred from workflow signals.",
|
|
1502
|
+
hints: ["phase", "gate", "transition", "triage", "dispatch", "verify", "qa", "land"],
|
|
1503
|
+
dimensionKeys: ["workflows"],
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
title: "CLI Command Surface",
|
|
1507
|
+
summary: "User-facing gxpm commands and command handlers inferred from CLI and API signals.",
|
|
1508
|
+
hints: ["cli:gxpm", "scripts/gxpm", "bin/gxpm", "command"],
|
|
1509
|
+
dimensionKeys: ["apis"],
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
title: "Hook Governance",
|
|
1513
|
+
summary: "Git and Codex hook surfaces inferred from hook paths, workflow signals, and template locations.",
|
|
1514
|
+
hints: ["hook", ".githooks", "pre-commit", "pre-push", "post-merge", "codex-hooks"],
|
|
1515
|
+
dimensionKeys: ["workflows"],
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
title: "Configuration And Workspace",
|
|
1519
|
+
summary: "Configuration, worktree, workspace, and environment behavior inferred from config signals.",
|
|
1520
|
+
hints: ["config", "worktree", "workspace", "env:"],
|
|
1521
|
+
dimensionKeys: ["config"],
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
title: "Host Adapters",
|
|
1525
|
+
summary: "Claude, Codex, and host installation boundaries inferred from host paths and adapter names.",
|
|
1526
|
+
hints: ["hosts/", "adapter", "claude", "codex"],
|
|
1527
|
+
dimensionKeys: ["structure", "symbols"],
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
title: "Tests And Verification",
|
|
1531
|
+
summary: "Test and validation coverage inferred from test paths and runner signals.",
|
|
1532
|
+
hints: ["path:test", "runner:test", "runner:describe", "verify", "test/"],
|
|
1533
|
+
dimensionKeys: ["tests"],
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
title: "Docs And Research",
|
|
1537
|
+
summary: "Architecture, governance, roadmap, and research material inferred from documentation signals.",
|
|
1538
|
+
hints: ["path:docs", "format:markdown", "readme", "architecture", "research", "roadmap"],
|
|
1539
|
+
dimensionKeys: ["docs"],
|
|
1540
|
+
},
|
|
1541
|
+
];
|
|
1542
|
+
|
|
1543
|
+
const NATIVE_WIKI_TOPICS: NativeWikiTopic[] = [
|
|
1544
|
+
{
|
|
1545
|
+
fileName: "Phase-Lifecycle.md",
|
|
1546
|
+
title: "Phase Lifecycle",
|
|
1547
|
+
summary: "How gxpm moves issue work through phase gates and artifact-backed transitions.",
|
|
1548
|
+
keywords: ["phase", "phases", "gate", "gates", "transition", "workflow", "lifecycle", "verify", "land"],
|
|
1549
|
+
sourcePaths: [
|
|
1550
|
+
"core/state.ts",
|
|
1551
|
+
"core/phase-gates.ts",
|
|
1552
|
+
"core/phase-artifact.ts",
|
|
1553
|
+
"core/triage.ts",
|
|
1554
|
+
"core/plan.ts",
|
|
1555
|
+
"core/dispatch.ts",
|
|
1556
|
+
"core/implement.ts",
|
|
1557
|
+
"core/ac-check.ts",
|
|
1558
|
+
"core/self-review.ts",
|
|
1559
|
+
"core/ship.ts",
|
|
1560
|
+
"core/pr-check.ts",
|
|
1561
|
+
"core/verify.ts",
|
|
1562
|
+
"core/qa.ts",
|
|
1563
|
+
"core/land.ts",
|
|
1564
|
+
"scripts/phase-artifact-commands.ts",
|
|
1565
|
+
],
|
|
1566
|
+
mermaid: renderPhaseLifecycleDiagram(),
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
fileName: "Artifact-System.md",
|
|
1570
|
+
title: "Artifact System",
|
|
1571
|
+
summary: "How gxpm stores phase evidence, acceptance contracts, checkpoints, and wiki context.",
|
|
1572
|
+
keywords: ["artifact", "artifacts", "evidence", "acceptance", "checkpoint", "context", "memory"],
|
|
1573
|
+
sourcePaths: [
|
|
1574
|
+
"core/artifacts.ts",
|
|
1575
|
+
"core/phase-artifact.ts",
|
|
1576
|
+
"core/state.ts",
|
|
1577
|
+
"scripts/phase-artifact-commands.ts",
|
|
1578
|
+
],
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
fileName: "Hook-Governance.md",
|
|
1582
|
+
title: "Hook Governance",
|
|
1583
|
+
summary: "How shell and Codex hooks guard branches, sessions, generated files, and workflow prompts.",
|
|
1584
|
+
keywords: ["hook", "hooks", "governance", "branch", "session", "codex", "pre-commit"],
|
|
1585
|
+
sourcePaths: [
|
|
1586
|
+
"core/gate.ts",
|
|
1587
|
+
"scripts/install-hooks.ts",
|
|
1588
|
+
"scripts/install-codex-hooks.ts",
|
|
1589
|
+
"templates/hooks/gxpm-commit-msg",
|
|
1590
|
+
"templates/hooks/gxpm-post-merge",
|
|
1591
|
+
"templates/hooks/gxpm-pre-commit",
|
|
1592
|
+
"templates/hooks/gxpm-pre-push",
|
|
1593
|
+
"templates/codex-hooks/pre-tool-use.sh",
|
|
1594
|
+
"templates/codex-hooks/session-start.sh",
|
|
1595
|
+
"templates/codex-hooks/user-prompt-submit.sh",
|
|
1596
|
+
],
|
|
1597
|
+
sourcePrefixes: [".githooks/", "templates/hooks/", "templates/codex-hooks/"],
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
fileName: "Config-Worktree.md",
|
|
1601
|
+
title: "Config And Worktree",
|
|
1602
|
+
summary: "How gxpm resolves repo config, workspace runtime behavior, and worktree policy.",
|
|
1603
|
+
keywords: ["config", "configuration", "worktree", "workspace", "branch", "main"],
|
|
1604
|
+
sourcePaths: [
|
|
1605
|
+
"core/config.ts",
|
|
1606
|
+
"core/workspace-runtime.ts",
|
|
1607
|
+
"AGENTS.md",
|
|
1608
|
+
"docs/governance/development-contract.md",
|
|
1609
|
+
],
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
fileName: "Native-Wiki.md",
|
|
1613
|
+
title: "Native Wiki",
|
|
1614
|
+
summary: "How gxpm initializes, updates, queries, and evaluates its first-party wiki.",
|
|
1615
|
+
keywords: ["wiki", "native", "index", "query", "context", "knowledge"],
|
|
1616
|
+
sourcePaths: [
|
|
1617
|
+
"core/wiki.ts",
|
|
1618
|
+
"core/wiki-native.ts",
|
|
1619
|
+
"test/wiki.test.ts",
|
|
1620
|
+
"docs/governance/development-contract.md",
|
|
1621
|
+
],
|
|
1622
|
+
},
|
|
1623
|
+
{
|
|
1624
|
+
fileName: "CLI-Surface.md",
|
|
1625
|
+
title: "CLI Surface",
|
|
1626
|
+
summary: "How gxpm exposes issue, artifact, wiki, hook, and workflow commands to agents.",
|
|
1627
|
+
keywords: ["cli", "command", "commands", "gxpm", "bin", "surface", "issue"],
|
|
1628
|
+
sourcePaths: ["scripts/gxpm.ts", "bin/gxpm", "README.md", "package.json"],
|
|
1629
|
+
},
|
|
1630
|
+
];
|
|
1631
|
+
|
|
1632
|
+
function renderNativeProjectTopics(
|
|
1633
|
+
index: NativeWikiIndex,
|
|
1634
|
+
graph: NativeWikiGraph,
|
|
1635
|
+
dimensions: NativeWikiDimensions,
|
|
1636
|
+
projectTopicClusters?: NativeWikiProjectTopicCluster[],
|
|
1637
|
+
) {
|
|
1638
|
+
const clusters = projectTopicClusters ?? buildNativeProjectTopicClusters(index, dimensions);
|
|
1639
|
+
const clusterSections = clusters.flatMap((cluster) => renderNativeProjectTopicCluster(cluster));
|
|
1640
|
+
const signalRows = renderNativeDimensionSignalSummary(dimensions);
|
|
1641
|
+
const hubRows = renderNativeGraphHubSummary(index, graph);
|
|
1642
|
+
|
|
1643
|
+
return [
|
|
1644
|
+
"# Project Topics",
|
|
1645
|
+
"",
|
|
1646
|
+
"Project-derived navigation generated from gxpm native dimensions. External wiki content is not read or copied for this page.",
|
|
1647
|
+
"",
|
|
1648
|
+
"## Inferred Topic Clusters",
|
|
1649
|
+
"",
|
|
1650
|
+
...(clusterSections.length > 0 ? clusterSections : ["- No topic clusters inferred from current dimensions.", ""]),
|
|
1651
|
+
"## Top Dimension Signals",
|
|
1652
|
+
"",
|
|
1653
|
+
"| Signal | Files |",
|
|
1654
|
+
"| --- | ---: |",
|
|
1655
|
+
...(signalRows.length > 0 ? signalRows : ["| - | 0 |"]),
|
|
1656
|
+
"",
|
|
1657
|
+
"## Import Hubs",
|
|
1658
|
+
"",
|
|
1659
|
+
"| File | Imports | Imported By |",
|
|
1660
|
+
"| --- | ---: | ---: |",
|
|
1661
|
+
...(hubRows.length > 0 ? hubRows : ["| - | 0 | 0 |"]),
|
|
1662
|
+
"",
|
|
1663
|
+
].join("\n");
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function nativeProjectTopicFileName(rule: NativeWikiProjectTopicRule) {
|
|
1667
|
+
return `${NATIVE_WIKI_PROJECT_TOPIC_DIR}/${nativeProjectTopicSlug(rule.title)}.md`;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function nativeProjectTopicDocPath(rule: NativeWikiProjectTopicRule) {
|
|
1671
|
+
return `${NATIVE_WIKI_CONTENT_ROOT}/${nativeProjectTopicFileName(rule)}`;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function nativeProjectTopicSlug(title: string) {
|
|
1675
|
+
return (
|
|
1676
|
+
title
|
|
1677
|
+
.toLowerCase()
|
|
1678
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1679
|
+
.replace(/^-+|-+$/g, "") || "topic"
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function buildNativeProjectTopicClusters(
|
|
1684
|
+
index: NativeWikiIndex,
|
|
1685
|
+
dimensions: NativeWikiDimensions,
|
|
1686
|
+
priorityDirs?: string[],
|
|
1687
|
+
) {
|
|
1688
|
+
const byPath = new Map(index.files.map((file) => [file.path, file]));
|
|
1689
|
+
const prioritySet = new Set((priorityDirs ?? []).map((d) => d.replace(/\/$/, "")));
|
|
1690
|
+
return NATIVE_WIKI_PROJECT_TOPIC_RULES.map((rule) => {
|
|
1691
|
+
const files = dimensions.files
|
|
1692
|
+
.map((entry) => {
|
|
1693
|
+
const file = byPath.get(entry.path);
|
|
1694
|
+
if (!file) return null;
|
|
1695
|
+
let score = scoreNativeProjectTopicFile(rule, entry);
|
|
1696
|
+
if (score <= 0) return null;
|
|
1697
|
+
for (const prefix of prioritySet) {
|
|
1698
|
+
if (entry.path.startsWith(prefix + "/")) {
|
|
1699
|
+
score += 5;
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
return {
|
|
1704
|
+
file,
|
|
1705
|
+
score,
|
|
1706
|
+
signals: matchingNativeProjectTopicSignals(rule, entry),
|
|
1707
|
+
};
|
|
1708
|
+
})
|
|
1709
|
+
.filter((entry): entry is NativeWikiProjectTopicFile => entry !== null)
|
|
1710
|
+
.sort((a, b) => b.score - a.score || a.file.path.localeCompare(b.file.path))
|
|
1711
|
+
.slice(0, 12);
|
|
1712
|
+
return { rule, files };
|
|
1713
|
+
}).filter((cluster) => cluster.files.length > 0);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function renderNativeProjectTopicCluster(cluster: NativeWikiProjectTopicCluster) {
|
|
1717
|
+
const rows = cluster.files.map((entry) => {
|
|
1718
|
+
const signals = entry.signals.slice(0, 8).map(escapeMarkdownCell).join(", ");
|
|
1719
|
+
return `| [${escapeMarkdownCell(entry.file.path)}](${nativeSourceLink(entry.file)}) | ${entry.score} | ${signals || "-"} |`;
|
|
1720
|
+
});
|
|
1721
|
+
return [
|
|
1722
|
+
`### [${cluster.rule.title}](${nativeFileUrl(nativeProjectTopicDocPath(cluster.rule))})`,
|
|
1723
|
+
"",
|
|
1724
|
+
cluster.rule.summary,
|
|
1725
|
+
"",
|
|
1726
|
+
"| File | Score | Matched Signals |",
|
|
1727
|
+
"| --- | ---: | --- |",
|
|
1728
|
+
...rows,
|
|
1729
|
+
"",
|
|
1730
|
+
];
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function renderNativeProjectTopicPage(cluster: NativeWikiProjectTopicCluster, graph: NativeWikiGraph) {
|
|
1734
|
+
const sourceLines = cluster.files.map((entry) => `- [${entry.file.path}](${nativeSourceLink(entry.file)})`);
|
|
1735
|
+
const rows = cluster.files.map((entry) => {
|
|
1736
|
+
const signals = entry.signals.slice(0, 12).map(escapeMarkdownCell).join(", ");
|
|
1737
|
+
return `| [${escapeMarkdownCell(entry.file.path)}](${nativeSourceLink(entry.file)}) | ${entry.file.language} | ${entry.score} | ${signals || "-"} |`;
|
|
1738
|
+
});
|
|
1739
|
+
const fileSet = new Set(cluster.files.map((entry) => entry.file.path));
|
|
1740
|
+
const edges = graph.edges
|
|
1741
|
+
.filter((edge) => fileSet.has(edge.from) || fileSet.has(edge.to))
|
|
1742
|
+
.slice(0, 30)
|
|
1743
|
+
.map((edge) => `- [${edge.from}](${nativeFileUrl(edge.from)}) -> [${edge.to}](${nativeFileUrl(edge.to)})`);
|
|
1744
|
+
|
|
1745
|
+
return [
|
|
1746
|
+
`# ${cluster.rule.title}`,
|
|
1747
|
+
"",
|
|
1748
|
+
cluster.rule.summary,
|
|
1749
|
+
"",
|
|
1750
|
+
"## Sources",
|
|
1751
|
+
"",
|
|
1752
|
+
...(sourceLines.length > 0 ? sourceLines : ["- No matching indexed source files."]),
|
|
1753
|
+
"",
|
|
1754
|
+
"## Matched Files",
|
|
1755
|
+
"",
|
|
1756
|
+
"| File | Language | Score | Matched Signals |",
|
|
1757
|
+
"| --- | --- | ---: | --- |",
|
|
1758
|
+
...(rows.length > 0 ? rows : ["| - | - | 0 | - |"]),
|
|
1759
|
+
"",
|
|
1760
|
+
"## Related Imports",
|
|
1761
|
+
"",
|
|
1762
|
+
...(edges.length > 0 ? edges : ["- none"]),
|
|
1763
|
+
"",
|
|
1764
|
+
"## Navigation",
|
|
1765
|
+
"",
|
|
1766
|
+
"- [Project Topics](file://.gxpm/wiki/content/Project-Topics.md)",
|
|
1767
|
+
"- [File Index](file://.gxpm/wiki/content/File-Index.md)",
|
|
1768
|
+
"- [Import Map](file://.gxpm/wiki/content/Import-Map.md)",
|
|
1769
|
+
"",
|
|
1770
|
+
].join("\n");
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function scoreNativeProjectTopicFile(rule: NativeWikiProjectTopicRule, entry: NativeWikiFileDimensions) {
|
|
1774
|
+
let hintScore = 0;
|
|
1775
|
+
const searchText = nativeProjectTopicSearchText(entry);
|
|
1776
|
+
for (const hint of rule.hints) {
|
|
1777
|
+
const normalized = hint.toLowerCase();
|
|
1778
|
+
if (entry.path.toLowerCase().includes(normalized)) hintScore += 5;
|
|
1779
|
+
if (searchText.includes(normalized)) hintScore += 3;
|
|
1780
|
+
}
|
|
1781
|
+
if (hintScore === 0) return 0;
|
|
1782
|
+
let dimensionScore = 0;
|
|
1783
|
+
for (const key of rule.dimensionKeys ?? []) {
|
|
1784
|
+
dimensionScore += entry.dimensions[key].length;
|
|
1785
|
+
}
|
|
1786
|
+
return hintScore + Math.min(dimensionScore, 8);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function matchingNativeProjectTopicSignals(rule: NativeWikiProjectTopicRule, entry: NativeWikiFileDimensions) {
|
|
1790
|
+
const hints = rule.hints.map((hint) => hint.toLowerCase());
|
|
1791
|
+
const signals = flattenNativeDimensionSignals(entry);
|
|
1792
|
+
const matched = signals.filter((signal) => {
|
|
1793
|
+
const lower = signal.toLowerCase();
|
|
1794
|
+
return hints.some((hint) => lower.includes(hint));
|
|
1795
|
+
});
|
|
1796
|
+
if (matched.length > 0) return matched;
|
|
1797
|
+
const scoped = signals.filter((signal) =>
|
|
1798
|
+
(rule.dimensionKeys ?? []).some((key) => signal.startsWith(`${key}:`)),
|
|
1799
|
+
);
|
|
1800
|
+
if (scoped.length > 0) return scoped;
|
|
1801
|
+
return signals;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function nativeProjectTopicSearchText(entry: NativeWikiFileDimensions) {
|
|
1805
|
+
return [entry.path, ...flattenNativeDimensionSignals(entry)].join(" ").toLowerCase();
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
function flattenNativeDimensionSignals(entry: NativeWikiFileDimensions) {
|
|
1809
|
+
return (Object.entries(entry.dimensions) as Array<[NativeWikiDimensionKey, string[]]>)
|
|
1810
|
+
.flatMap(([key, signals]) => signals.map((signal) => `${key}:${signal}`));
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function renderNativeDimensionSignalSummary(dimensions: NativeWikiDimensions) {
|
|
1814
|
+
const counts = new Map<string, number>();
|
|
1815
|
+
for (const entry of dimensions.files) {
|
|
1816
|
+
for (const signal of flattenNativeDimensionSignals(entry)) {
|
|
1817
|
+
counts.set(signal, (counts.get(signal) ?? 0) + 1);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return [...counts.entries()]
|
|
1821
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
1822
|
+
.slice(0, 20)
|
|
1823
|
+
.map(([signal, count]) => `| ${escapeMarkdownCell(signal)} | ${count} |`);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function renderNativeGraphHubSummary(index: NativeWikiIndex, graph: NativeWikiGraph) {
|
|
1827
|
+
const outgoing = new Map<string, number>();
|
|
1828
|
+
const incoming = new Map<string, number>();
|
|
1829
|
+
for (const edge of graph.edges) {
|
|
1830
|
+
outgoing.set(edge.from, (outgoing.get(edge.from) ?? 0) + 1);
|
|
1831
|
+
incoming.set(edge.to, (incoming.get(edge.to) ?? 0) + 1);
|
|
1832
|
+
}
|
|
1833
|
+
return index.files
|
|
1834
|
+
.map((file) => ({
|
|
1835
|
+
file,
|
|
1836
|
+
outgoing: outgoing.get(file.path) ?? 0,
|
|
1837
|
+
incoming: incoming.get(file.path) ?? 0,
|
|
1838
|
+
}))
|
|
1839
|
+
.filter((entry) => entry.outgoing > 0 || entry.incoming > 0)
|
|
1840
|
+
.sort(
|
|
1841
|
+
(a, b) =>
|
|
1842
|
+
b.outgoing + b.incoming - (a.outgoing + a.incoming) ||
|
|
1843
|
+
b.incoming - a.incoming ||
|
|
1844
|
+
a.file.path.localeCompare(b.file.path),
|
|
1845
|
+
)
|
|
1846
|
+
.slice(0, 15)
|
|
1847
|
+
.map(
|
|
1848
|
+
(entry) =>
|
|
1849
|
+
`| [${escapeMarkdownCell(entry.file.path)}](${nativeSourceLink(entry.file)}) | ${entry.outgoing} | ${entry.incoming} |`,
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function renderNativeTopicDoc(topic: NativeWikiTopic, index: NativeWikiIndex, graph: NativeWikiGraph) {
|
|
1854
|
+
const files = topicFiles(topic, index);
|
|
1855
|
+
const citeLines = files.map((file) => `- [${file.path}](${nativeSourceLink(file)})`);
|
|
1856
|
+
const rows = files.map((file) => {
|
|
1857
|
+
const signals = [...file.exports, ...file.headings].slice(0, 8).map(escapeMarkdownCell).join(", ");
|
|
1858
|
+
return `| [${escapeMarkdownCell(file.path)}](${nativeSourceLink(file)}) | ${file.language} | ${signals || "-"} |`;
|
|
1859
|
+
});
|
|
1860
|
+
const fileSet = new Set(files.map((file) => file.path));
|
|
1861
|
+
const edges = graph.edges
|
|
1862
|
+
.filter((edge) => fileSet.has(edge.from) || fileSet.has(edge.to))
|
|
1863
|
+
.slice(0, 30)
|
|
1864
|
+
.map((edge) => `- [${edge.from}](${nativeFileUrl(edge.from)}) -> [${edge.to}](${nativeFileUrl(edge.to)})`);
|
|
1865
|
+
|
|
1866
|
+
return [
|
|
1867
|
+
`# ${topic.title}`,
|
|
1868
|
+
"",
|
|
1869
|
+
topic.summary,
|
|
1870
|
+
"",
|
|
1871
|
+
"## Sources",
|
|
1872
|
+
"",
|
|
1873
|
+
...(citeLines.length > 0 ? citeLines : ["- No matching indexed source files."]),
|
|
1874
|
+
"",
|
|
1875
|
+
...(topic.mermaid ? ["## Flow", "", "```mermaid", topic.mermaid, "```", ""] : []),
|
|
1876
|
+
"## Source Files",
|
|
1877
|
+
"",
|
|
1878
|
+
"| File | Language | Signals |",
|
|
1879
|
+
"| --- | --- | --- |",
|
|
1880
|
+
...(rows.length > 0 ? rows : ["| - | - | - |"]),
|
|
1881
|
+
"",
|
|
1882
|
+
"## Related Imports",
|
|
1883
|
+
"",
|
|
1884
|
+
...(edges.length > 0 ? edges : ["- none"]),
|
|
1885
|
+
"",
|
|
1886
|
+
].join("\n");
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function topicFiles(topic: NativeWikiTopic, index: NativeWikiIndex) {
|
|
1890
|
+
return index.files
|
|
1891
|
+
.filter((file) => nativeTopicOwnsPath(topic, file.path))
|
|
1892
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function nativeTopicOwnsPath(topic: NativeWikiTopic, path: string) {
|
|
1896
|
+
return topic.sourcePaths.includes(path) || (topic.sourcePrefixes ?? []).some((prefix) => path.startsWith(prefix));
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function nativeSourceLink(file: NativeWikiFileEntry) {
|
|
1900
|
+
const endLine = Math.max(1, file.lineCount || 1);
|
|
1901
|
+
return `${nativeFileUrl(file.path)}#L1-L${endLine}`;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function nativeFileUrl(path: string, line?: number) {
|
|
1905
|
+
return `file://${encodeFileUrlPath(path)}${line ? `#L${line}` : ""}`;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function encodeFileUrlPath(path: string) {
|
|
1909
|
+
return path
|
|
1910
|
+
.split("/")
|
|
1911
|
+
.map((part) =>
|
|
1912
|
+
encodeURIComponent(part).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`),
|
|
1913
|
+
)
|
|
1914
|
+
.join("/");
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function decodeFileUrlPath(path: string) {
|
|
1918
|
+
try {
|
|
1919
|
+
return decodeURIComponent(path);
|
|
1920
|
+
} catch {
|
|
1921
|
+
return path;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function renderPhaseLifecycleDiagram() {
|
|
1926
|
+
const edges = GXPM_PHASES.slice(0, -1).map((phase, index) => ` ${phase} --> ${GXPM_PHASES[index + 1]}`);
|
|
1927
|
+
return ["flowchart LR", ...edges].join("\n");
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
function readNativeWikiIndex(root: string): NativeWikiIndex {
|
|
1931
|
+
const path = join(root, NATIVE_WIKI_INDEX_PATH);
|
|
1932
|
+
if (!existsSync(path)) {
|
|
1933
|
+
throw new Error("Native gxpm wiki index not found. Run `gxpm wiki init` first.");
|
|
1934
|
+
}
|
|
1935
|
+
return normalizeNativeWikiIndex(JSON.parse(readFileSync(path, "utf8")) as NativeWikiIndex);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function readNativeWikiStateIfPresent(root: string): NativeWikiState | null {
|
|
1939
|
+
const path = join(root, NATIVE_WIKI_STATE_PATH);
|
|
1940
|
+
if (!existsSync(path)) return null;
|
|
1941
|
+
try {
|
|
1942
|
+
return JSON.parse(readFileSync(path, "utf8")) as NativeWikiState;
|
|
1943
|
+
} catch {
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function readNativeWikiIndexIfPresent(root: string): NativeWikiIndex | null {
|
|
1949
|
+
const path = join(root, NATIVE_WIKI_INDEX_PATH);
|
|
1950
|
+
if (!existsSync(path)) return null;
|
|
1951
|
+
try {
|
|
1952
|
+
return normalizeNativeWikiIndex(JSON.parse(readFileSync(path, "utf8")) as NativeWikiIndex);
|
|
1953
|
+
} catch {
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function readNativeWikiGraphIfPresent(root: string): NativeWikiGraph | null {
|
|
1959
|
+
const path = join(root, NATIVE_WIKI_GRAPH_PATH);
|
|
1960
|
+
if (!existsSync(path)) return null;
|
|
1961
|
+
try {
|
|
1962
|
+
return JSON.parse(readFileSync(path, "utf8")) as NativeWikiGraph;
|
|
1963
|
+
} catch {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function readNativeWikiDimensionsIfPresent(root: string): NativeWikiDimensions | null {
|
|
1969
|
+
const path = join(root, NATIVE_WIKI_DIMENSIONS_PATH);
|
|
1970
|
+
if (!existsSync(path)) return null;
|
|
1971
|
+
try {
|
|
1972
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
1973
|
+
if (!isNativeWikiDimensions(value)) return null;
|
|
1974
|
+
return value;
|
|
1975
|
+
} catch {
|
|
1976
|
+
return null;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
function isNativeWikiDimensions(value: unknown): value is NativeWikiDimensions {
|
|
1981
|
+
if (!value || typeof value !== "object") return false;
|
|
1982
|
+
const candidate = value as Partial<NativeWikiDimensions>;
|
|
1983
|
+
return (
|
|
1984
|
+
candidate.schemaVersion === 1 &&
|
|
1985
|
+
candidate.provider === "gxpm" &&
|
|
1986
|
+
typeof candidate.generatedAt === "string" &&
|
|
1987
|
+
Array.isArray(candidate.files)
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function normalizeNativeWikiIndex(index: NativeWikiIndex): NativeWikiIndex {
|
|
1992
|
+
return {
|
|
1993
|
+
...index,
|
|
1994
|
+
files: index.files.map((file) => ({
|
|
1995
|
+
...file,
|
|
1996
|
+
lineCount: typeof file.lineCount === "number" ? file.lineCount : 0,
|
|
1997
|
+
contentHash: typeof file.contentHash === "string" ? file.contentHash : "",
|
|
1998
|
+
})),
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function listNativeWikiDocs(root: string) {
|
|
2003
|
+
const docs: string[] = [];
|
|
2004
|
+
walkFiles(join(root, NATIVE_WIKI_CONTENT_ROOT), (file) => {
|
|
2005
|
+
if (file.endsWith(".md")) docs.push(toRepoPath(root, file));
|
|
2006
|
+
});
|
|
2007
|
+
return docs.sort();
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function changedNativeFiles(root: string, index: NativeWikiIndex) {
|
|
2011
|
+
const indexed = new Map(index.files.map((file) => [file.path, file]));
|
|
2012
|
+
const trackedPaths = listNativeRepoFiles(root).map((file) => toRepoPath(root, file));
|
|
2013
|
+
const tracked = new Set(trackedPaths);
|
|
2014
|
+
const changed = new Set<string>();
|
|
2015
|
+
for (const path of trackedPaths) {
|
|
2016
|
+
const indexedFile = indexed.get(path);
|
|
2017
|
+
if (!indexedFile) {
|
|
2018
|
+
changed.add(path);
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
try {
|
|
2022
|
+
const stat = statSync(join(root, path));
|
|
2023
|
+
if (stat.size !== indexedFile.sizeBytes || Math.abs(stat.mtimeMs - indexedFile.mtimeMs) > 1) {
|
|
2024
|
+
changed.add(path);
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
// If mtime/size match but contentHash is present, verify hash to catch
|
|
2028
|
+
// cases where mtime was preserved (e.g. git checkout, patch -p0)
|
|
2029
|
+
if (indexedFile.contentHash) {
|
|
2030
|
+
const content = safeRead(join(root, path));
|
|
2031
|
+
if (sha256Hex(content) !== indexedFile.contentHash) {
|
|
2032
|
+
changed.add(path);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
} catch {
|
|
2036
|
+
changed.add(path);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
for (const path of indexed.keys()) {
|
|
2040
|
+
if (!tracked.has(path)) changed.add(path);
|
|
2041
|
+
}
|
|
2042
|
+
return [...changed].sort();
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
function nativeWikiStatusPaths() {
|
|
2046
|
+
return {
|
|
2047
|
+
state: NATIVE_WIKI_STATE_PATH,
|
|
2048
|
+
index: NATIVE_WIKI_INDEX_PATH,
|
|
2049
|
+
graph: NATIVE_WIKI_GRAPH_PATH,
|
|
2050
|
+
dimensions: NATIVE_WIKI_DIMENSIONS_PATH,
|
|
2051
|
+
contentRoot: NATIVE_WIKI_CONTENT_ROOT,
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function nativeWikiStatusCommands() {
|
|
2056
|
+
return {
|
|
2057
|
+
init: "gxpm wiki init",
|
|
2058
|
+
update: "gxpm wiki update",
|
|
2059
|
+
query: "gxpm wiki query <text>",
|
|
2060
|
+
context: "gxpm wiki context <issue-id>",
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function suggestedNativeDocs(root: string, contextFiles: string[], tokens: string[] = []) {
|
|
2065
|
+
const allDocs = new Set(listNativeWikiDocs(root));
|
|
2066
|
+
const scoredTopicDocs = NATIVE_WIKI_TOPICS.map((topic, index) => ({
|
|
2067
|
+
path: `${NATIVE_WIKI_CONTENT_ROOT}/${topic.fileName}`,
|
|
2068
|
+
score: scoreNativeTopicSuggestion(topic, contextFiles, tokens),
|
|
2069
|
+
index,
|
|
2070
|
+
}))
|
|
2071
|
+
.filter((topic) => topic.score > 0 && allDocs.has(topic.path))
|
|
2072
|
+
.sort((a, b) => b.score - a.score || a.index - b.index || a.path.localeCompare(b.path));
|
|
2073
|
+
const strongTopicDocs = scoredTopicDocs.filter((topic) => topic.score >= 20).map((topic) => topic.path);
|
|
2074
|
+
const weakTopicDocs = scoredTopicDocs.filter((topic) => topic.score < 20).map((topic) => topic.path);
|
|
2075
|
+
|
|
2076
|
+
const projectTopicDoc = `${NATIVE_WIKI_CONTENT_ROOT}/${NATIVE_WIKI_PROJECT_TOPIC_FILE}`;
|
|
2077
|
+
const projectTopicSuggestions = computeNativeProjectTopicSuggestions(
|
|
2078
|
+
readNativeWikiDimensionsIfPresent(root),
|
|
2079
|
+
contextFiles,
|
|
2080
|
+
allDocs,
|
|
2081
|
+
);
|
|
2082
|
+
const projectTopicDocs =
|
|
2083
|
+
allDocs.has(projectTopicDoc) && projectTopicSuggestions.hasMatches
|
|
2084
|
+
? [projectTopicDoc]
|
|
2085
|
+
: [];
|
|
2086
|
+
|
|
2087
|
+
const genericDocs: string[] = [];
|
|
2088
|
+
walkFiles(join(root, NATIVE_WIKI_CONTENT_ROOT), (file) => {
|
|
2089
|
+
if (!file.endsWith(".md")) return;
|
|
2090
|
+
const repoPath = toRepoPath(root, file);
|
|
2091
|
+
if (isNativeTopicDoc(repoPath) || repoPath.endsWith("/Overview.md")) return;
|
|
2092
|
+
const cited = extractCitedFiles(safeRead(file));
|
|
2093
|
+
if (cited.some((path) => contextFiles.includes(path))) genericDocs.push(repoPath);
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
const overview = allDocs.has(`${NATIVE_WIKI_CONTENT_ROOT}/Overview.md`)
|
|
2097
|
+
? [`${NATIVE_WIKI_CONTENT_ROOT}/Overview.md`]
|
|
2098
|
+
: [];
|
|
2099
|
+
return dedupeBy(
|
|
2100
|
+
[
|
|
2101
|
+
...strongTopicDocs,
|
|
2102
|
+
...projectTopicSuggestions.paths,
|
|
2103
|
+
...projectTopicDocs,
|
|
2104
|
+
...weakTopicDocs,
|
|
2105
|
+
...genericDocs.sort(),
|
|
2106
|
+
...overview,
|
|
2107
|
+
],
|
|
2108
|
+
(path) => path,
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
function computeNativeProjectTopicSuggestions(
|
|
2113
|
+
dimensions: NativeWikiDimensions | null,
|
|
2114
|
+
contextFiles: string[],
|
|
2115
|
+
allDocs: Set<string>,
|
|
2116
|
+
): { paths: string[]; hasMatches: boolean } {
|
|
2117
|
+
if (!dimensions || contextFiles.length === 0) return { paths: [], hasMatches: false };
|
|
2118
|
+
const contextFileSet = new Set(contextFiles);
|
|
2119
|
+
const scored = NATIVE_WIKI_PROJECT_TOPIC_RULES.map((rule, index) => ({
|
|
2120
|
+
path: nativeProjectTopicDocPath(rule),
|
|
2121
|
+
score: dimensions.files
|
|
2122
|
+
.filter((entry) => contextFileSet.has(entry.path))
|
|
2123
|
+
.reduce((sum, entry) => sum + scoreNativeProjectTopicFile(rule, entry), 0),
|
|
2124
|
+
index,
|
|
2125
|
+
})).filter((entry) => entry.score > 0);
|
|
2126
|
+
return {
|
|
2127
|
+
hasMatches: scored.length > 0,
|
|
2128
|
+
paths: scored
|
|
2129
|
+
.filter((entry) => allDocs.has(entry.path))
|
|
2130
|
+
.sort((a, b) => b.score - a.score || a.index - b.index || a.path.localeCompare(b.path))
|
|
2131
|
+
.map((entry) => entry.path),
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function nativeFileMatches(file: NativeWikiFileEntry, tokens: string[]) {
|
|
2136
|
+
const matches: Array<{ label: string; score: number; line?: number }> = [];
|
|
2137
|
+
const path = file.path.toLowerCase();
|
|
2138
|
+
const exports = file.exports.join(" ").toLowerCase();
|
|
2139
|
+
const imports = file.imports.join(" ").toLowerCase();
|
|
2140
|
+
const headings = file.headings.join(" ").toLowerCase();
|
|
2141
|
+
const symbols = file.symbols;
|
|
2142
|
+
const symbolNames = symbols.map((s) => s.name.toLowerCase()).join(" ");
|
|
2143
|
+
for (const token of tokens) {
|
|
2144
|
+
if (path.includes(token)) matches.push({ label: `path:${token}`, score: 5 });
|
|
2145
|
+
if (exports.includes(token)) matches.push({ label: `export:${token}`, score: 4 });
|
|
2146
|
+
if (headings.includes(token)) matches.push({ label: `heading:${token}`, score: 3 });
|
|
2147
|
+
if (symbolNames.includes(token)) {
|
|
2148
|
+
const match = symbols.find((s) => s.name.toLowerCase().includes(token));
|
|
2149
|
+
matches.push({ label: `symbol:${token}`, score: 4, line: match?.line });
|
|
2150
|
+
}
|
|
2151
|
+
if (imports.includes(token)) matches.push({ label: `import:${token}`, score: 1 });
|
|
2152
|
+
}
|
|
2153
|
+
return matches;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function scoreNativeTopicSuggestion(topic: NativeWikiTopic, contextFiles: string[], tokens: string[]) {
|
|
2157
|
+
let score = 0;
|
|
2158
|
+
for (const file of contextFiles) {
|
|
2159
|
+
if (nativeTopicOwnsPath(topic, file)) score += 20;
|
|
2160
|
+
}
|
|
2161
|
+
for (const token of tokens) {
|
|
2162
|
+
if (topic.keywords.includes(token)) score += 3;
|
|
2163
|
+
if (topic.sourcePaths.some((path) => path.toLowerCase().includes(token))) score += 1;
|
|
2164
|
+
}
|
|
2165
|
+
return score;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function isNativeTopicDoc(repoPath: string) {
|
|
2169
|
+
return NATIVE_WIKI_TOPICS.some((topic) => repoPath === `${NATIVE_WIKI_CONTENT_ROOT}/${topic.fileName}`);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
function tokenizeQuery(query: string) {
|
|
2173
|
+
return query
|
|
2174
|
+
.toLowerCase()
|
|
2175
|
+
.split(/[^a-z0-9_]+/)
|
|
2176
|
+
.map((token) => token.trim())
|
|
2177
|
+
.filter((token) => token.length >= 2);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
function extractExports(content: string) {
|
|
2181
|
+
const exports = new Set<string>();
|
|
2182
|
+
collectRegex(content, /^\s*export\s+(?:async\s+)?(?:function|class|interface|type|const|let|var|enum)\s+([A-Za-z_$][\w$]*)/gm, exports);
|
|
2183
|
+
collectRegex(content, /^\s*export\s+default\s+(?:async\s+)?(?:function|class)?\s*([A-Za-z_$][\w$]*)?/gm, exports, "default");
|
|
2184
|
+
const namedExportRe = /^\s*export\s*\{([^}]+)\}/gm;
|
|
2185
|
+
let match: RegExpExecArray | null;
|
|
2186
|
+
while ((match = namedExportRe.exec(content)) !== null) {
|
|
2187
|
+
for (const raw of match[1].split(",")) {
|
|
2188
|
+
const name = raw.trim().split(/\s+as\s+/i)[0]?.trim();
|
|
2189
|
+
if (name) exports.add(name);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
return [...exports].sort();
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
function extractImports(content: string) {
|
|
2196
|
+
const imports = new Set<string>();
|
|
2197
|
+
const staticImportRe = /\b(?:import|export)\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?["']([^"']+)["']/g;
|
|
2198
|
+
const dynamicImportRe = /\b(?:import|require)\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
2199
|
+
collectRegex(content, staticImportRe, imports);
|
|
2200
|
+
collectRegex(content, dynamicImportRe, imports);
|
|
2201
|
+
return [...imports].sort();
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
function extractMarkdownHeadings(content: string) {
|
|
2205
|
+
const headings = new Set<string>();
|
|
2206
|
+
collectRegex(content, /^#{1,6}\s+(.+)$/gm, headings);
|
|
2207
|
+
return [...headings].sort();
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
function extractNativeSymbols(filePath: string, content: string): Array<{ name: string; kind: string; line: number }> {
|
|
2211
|
+
const ext = extname(filePath);
|
|
2212
|
+
if (ext !== ".ts" && ext !== ".tsx" && ext !== ".js" && ext !== ".jsx" && ext !== ".mjs" && ext !== ".cjs") {
|
|
2213
|
+
return [];
|
|
2214
|
+
}
|
|
2215
|
+
let scriptKind: ts.ScriptKind;
|
|
2216
|
+
switch (ext) {
|
|
2217
|
+
case ".tsx": scriptKind = ts.ScriptKind.TSX; break;
|
|
2218
|
+
case ".jsx": scriptKind = ts.ScriptKind.JSX; break;
|
|
2219
|
+
case ".js": scriptKind = ts.ScriptKind.JS; break;
|
|
2220
|
+
default: scriptKind = ts.ScriptKind.TS; break;
|
|
2221
|
+
}
|
|
2222
|
+
const source = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
2223
|
+
const symbols: Array<{ name: string; kind: string; line: number }> = [];
|
|
2224
|
+
function visit(node: ts.Node) {
|
|
2225
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
2226
|
+
const pos = source.getLineAndCharacterOfPosition(node.getStart());
|
|
2227
|
+
symbols.push({ name: node.name.text, kind: "function", line: pos.line + 1 });
|
|
2228
|
+
} else if (ts.isClassDeclaration(node) && node.name) {
|
|
2229
|
+
const pos = source.getLineAndCharacterOfPosition(node.getStart());
|
|
2230
|
+
symbols.push({ name: node.name.text, kind: "class", line: pos.line + 1 });
|
|
2231
|
+
} else if (ts.isInterfaceDeclaration(node)) {
|
|
2232
|
+
const pos = source.getLineAndCharacterOfPosition(node.getStart());
|
|
2233
|
+
symbols.push({ name: node.name.text, kind: "interface", line: pos.line + 1 });
|
|
2234
|
+
} else if (ts.isTypeAliasDeclaration(node)) {
|
|
2235
|
+
const pos = source.getLineAndCharacterOfPosition(node.getStart());
|
|
2236
|
+
symbols.push({ name: node.name.text, kind: "type", line: pos.line + 1 });
|
|
2237
|
+
} else if (ts.isEnumDeclaration(node)) {
|
|
2238
|
+
const pos = source.getLineAndCharacterOfPosition(node.getStart());
|
|
2239
|
+
symbols.push({ name: node.name.text, kind: "enum", line: pos.line + 1 });
|
|
2240
|
+
} else if (ts.isVariableStatement(node)) {
|
|
2241
|
+
for (const decl of node.declarationList.declarations) {
|
|
2242
|
+
if (ts.isIdentifier(decl.name)) {
|
|
2243
|
+
const pos = source.getLineAndCharacterOfPosition(decl.getStart());
|
|
2244
|
+
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
2245
|
+
symbols.push({ name: decl.name.text, kind: isConst ? "const" : "variable", line: pos.line + 1 });
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
ts.forEachChild(node, visit);
|
|
2250
|
+
}
|
|
2251
|
+
visit(source);
|
|
2252
|
+
return symbols;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
function collectRegex(content: string, regex: RegExp, values: Set<string>, fallback?: string, prefix = "") {
|
|
2256
|
+
let match: RegExpExecArray | null;
|
|
2257
|
+
while ((match = regex.exec(content)) !== null) {
|
|
2258
|
+
const value = match[1]?.trim() || fallback || match[0]?.trim();
|
|
2259
|
+
if (value) values.add(`${prefix}${value}`);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
function resolveNativeImport(from: string, specifier: string, filePaths: Set<string>) {
|
|
2264
|
+
if (!specifier.startsWith(".")) return null;
|
|
2265
|
+
const base = normalizeRepoPath(join(dirname(from), specifier));
|
|
2266
|
+
const candidates = [
|
|
2267
|
+
base,
|
|
2268
|
+
...NATIVE_IMPORT_RESOLVABLE_EXTENSIONS.map((extension) => `${base}${extension}`),
|
|
2269
|
+
...NATIVE_IMPORT_RESOLVABLE_EXTENSIONS.map((extension) => `${base}/index${extension}`),
|
|
2270
|
+
];
|
|
2271
|
+
return candidates.find((candidate) => filePaths.has(candidate)) ?? null;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function languageForPath(path: string) {
|
|
2275
|
+
const ext = extname(path).toLowerCase();
|
|
2276
|
+
if (ext === ".ts" || ext === ".tsx") return "typescript";
|
|
2277
|
+
if (ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") return "javascript";
|
|
2278
|
+
if (ext === ".md") return "markdown";
|
|
2279
|
+
if (ext === ".json") return "json";
|
|
2280
|
+
if (ext === ".sh") return "shell";
|
|
2281
|
+
if (ext === ".yaml" || ext === ".yml") return "yaml";
|
|
2282
|
+
return ext.replace(/^\./, "") || "text";
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function currentGitCommit(root: string) {
|
|
2286
|
+
const result = Bun.spawnSync({
|
|
2287
|
+
cmd: ["git", "rev-parse", "HEAD"],
|
|
2288
|
+
cwd: root,
|
|
2289
|
+
stdout: "pipe",
|
|
2290
|
+
stderr: "pipe",
|
|
2291
|
+
});
|
|
2292
|
+
if (result.exitCode !== 0) return null;
|
|
2293
|
+
const value = result.stdout.toString().trim();
|
|
2294
|
+
return value || null;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
function walkNativeRepoFiles(dir: string, visit: (file: string) => void) {
|
|
2298
|
+
if (!existsSync(dir)) return;
|
|
2299
|
+
let entries;
|
|
2300
|
+
try {
|
|
2301
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2302
|
+
} catch {
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
for (const entry of entries) {
|
|
2306
|
+
if (entry.isDirectory()) {
|
|
2307
|
+
if (!NATIVE_SKIP_DIRS.has(entry.name)) walkNativeRepoFiles(join(dir, entry.name), visit);
|
|
2308
|
+
} else if (entry.isFile()) {
|
|
2309
|
+
visit(join(dir, entry.name));
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function writeJson(path: string, value: unknown) {
|
|
2315
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2316
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function definedOnly<T extends Record<string, unknown>>(value: T): Partial<T> {
|
|
2320
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as Partial<T>;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
function dedupeBy<T>(values: T[], key: (value: T) => string) {
|
|
2324
|
+
const seen = new Set<string>();
|
|
2325
|
+
const result: T[] = [];
|
|
2326
|
+
for (const value of values) {
|
|
2327
|
+
const id = key(value);
|
|
2328
|
+
if (seen.has(id)) continue;
|
|
2329
|
+
seen.add(id);
|
|
2330
|
+
result.push(value);
|
|
2331
|
+
}
|
|
2332
|
+
return result;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
function pushMapValue<K, V>(map: Map<K, V[]>, key: K, value: V) {
|
|
2336
|
+
const values = map.get(key) ?? [];
|
|
2337
|
+
values.push(value);
|
|
2338
|
+
map.set(key, values);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function countBy<T>(values: T[], key: (value: T) => string) {
|
|
2342
|
+
const counts = new Map<string, number>();
|
|
2343
|
+
for (const value of values) {
|
|
2344
|
+
const id = key(value);
|
|
2345
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
2346
|
+
}
|
|
2347
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function normalizeRepoPath(path: string) {
|
|
2351
|
+
return path.split(sep).join("/").replace(/^\.\//, "");
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function walkDirs(dir: string, visit: (dir: string) => void) {
|
|
2355
|
+
if (!existsSync(dir)) return;
|
|
2356
|
+
visit(dir);
|
|
2357
|
+
let entries;
|
|
2358
|
+
try {
|
|
2359
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2360
|
+
} catch {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
for (const entry of entries) {
|
|
2364
|
+
if (entry.isDirectory()) walkDirs(join(dir, entry.name), visit);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function walkFiles(dir: string, visit: (file: string) => void) {
|
|
2369
|
+
if (!existsSync(dir)) return;
|
|
2370
|
+
let entries;
|
|
2371
|
+
try {
|
|
2372
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2373
|
+
} catch {
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
for (const entry of entries) {
|
|
2377
|
+
const path = join(dir, entry.name);
|
|
2378
|
+
if (entry.isDirectory()) walkFiles(path, visit);
|
|
2379
|
+
else if (entry.isFile()) visit(path);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function safeRead(path: string) {
|
|
2384
|
+
try {
|
|
2385
|
+
return readFileSync(path, "utf8");
|
|
2386
|
+
} catch {
|
|
2387
|
+
return "";
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
function isLikelyTextFile(path: string) {
|
|
2392
|
+
let fd: number | undefined;
|
|
2393
|
+
try {
|
|
2394
|
+
fd = openSync(path, "r");
|
|
2395
|
+
const sample = Buffer.alloc(4096);
|
|
2396
|
+
const bytesRead = readSync(fd, sample, 0, sample.length, 0);
|
|
2397
|
+
const bytes = sample.subarray(0, bytesRead);
|
|
2398
|
+
for (const byte of bytes) {
|
|
2399
|
+
if (byte === 0) return false;
|
|
2400
|
+
if (byte < 7 || (byte > 13 && byte < 32)) return false;
|
|
2401
|
+
}
|
|
2402
|
+
return true;
|
|
2403
|
+
} catch {
|
|
2404
|
+
return false;
|
|
2405
|
+
} finally {
|
|
2406
|
+
if (fd !== undefined) closeSync(fd);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
function countLines(content: string) {
|
|
2411
|
+
if (content.length === 0) return 0;
|
|
2412
|
+
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2413
|
+
const trimmedTrailingNewline = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
2414
|
+
return trimmedTrailingNewline.length === 0 ? 1 : trimmedTrailingNewline.split("\n").length;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
function toRepoPath(root: string, path: string) {
|
|
2418
|
+
return relative(root, path).split(sep).join("/");
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
function escapeMarkdownCell(value: string) {
|
|
2422
|
+
return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|");
|
|
2423
|
+
}
|