onto-mcp 0.3.0 → 0.3.2

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.
Files changed (61) hide show
  1. package/.onto/authority/core-lexicon.yaml +12 -0
  2. package/.onto/domains/software-engineering/competency_qs.md +192 -63
  3. package/.onto/domains/software-engineering/concepts.md +67 -5
  4. package/.onto/domains/software-engineering/conciseness_rules.md +22 -2
  5. package/.onto/domains/software-engineering/dependency_rules.md +78 -8
  6. package/.onto/domains/software-engineering/domain_scope.md +181 -150
  7. package/.onto/domains/software-engineering/extension_cases.md +318 -542
  8. package/.onto/domains/software-engineering/logic_rules.md +75 -3
  9. package/.onto/domains/software-engineering/problem_framing_profile.md +29 -2
  10. package/.onto/domains/software-engineering/prompt_interface.md +122 -0
  11. package/.onto/domains/software-engineering/structure_spec.md +53 -4
  12. package/.onto/principles/llm-native-development-guideline.md +20 -0
  13. package/.onto/principles/productization-charter.md +6 -0
  14. package/.onto/processes/evolve/material-kind-adapter-contract.md +6 -0
  15. package/.onto/processes/reconstruct/reconstruct-boundary-contract.md +468 -81
  16. package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +177 -0
  17. package/.onto/processes/reconstruct/source-profile-contract.md +39 -6
  18. package/.onto/processes/reconstruct/top-level-concept-discovery-contract.md +387 -0
  19. package/.onto/processes/review/binding-contract.md +8 -0
  20. package/.onto/processes/review/lens-registry.md +16 -0
  21. package/.onto/processes/review/pre-dispatch-contracts.md +34 -13
  22. package/.onto/processes/review/productized-live-path.md +3 -1
  23. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +185 -0
  24. package/.onto/processes/shared/target-material-kind-contract.md +24 -2
  25. package/.onto/roles/axiology.md +7 -2
  26. package/AGENTS.md +4 -2
  27. package/README.md +52 -29
  28. package/dist/core-api/reconstruct-api.js +92 -5
  29. package/dist/core-api/review-api.js +1744 -371
  30. package/dist/core-runtime/cli/mock-review-unit-executor.js +17 -0
  31. package/dist/core-runtime/cli/render-review-final-output.js +9 -0
  32. package/dist/core-runtime/cli/review-invoke.js +387 -55
  33. package/dist/core-runtime/cli/run-review-prompt-execution.js +361 -90
  34. package/dist/core-runtime/path-boundary.js +58 -0
  35. package/dist/core-runtime/pipeline-execution-ledger.js +100 -0
  36. package/dist/core-runtime/reconstruct/artifact-types.js +33 -1
  37. package/dist/core-runtime/reconstruct/materialize-preparation.js +54 -4
  38. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +342 -0
  39. package/dist/core-runtime/reconstruct/post-seed-validation.js +630 -0
  40. package/dist/core-runtime/reconstruct/record.js +105 -1
  41. package/dist/core-runtime/reconstruct/run.js +1594 -38
  42. package/dist/core-runtime/reconstruct/seed-candidate-validation.js +29 -0
  43. package/dist/core-runtime/review/continuation-plan.js +160 -0
  44. package/dist/core-runtime/review/execution-plan-boundary.js +123 -0
  45. package/dist/core-runtime/review/materializers.js +8 -3
  46. package/dist/core-runtime/review/pipeline-execution-ledger.js +250 -0
  47. package/dist/core-runtime/review/review-artifact-utils.js +15 -2
  48. package/dist/core-runtime/review/review-invocation-runner.js +604 -0
  49. package/dist/core-runtime/target-material-kind.js +43 -5
  50. package/dist/mcp/server.js +289 -59
  51. package/dist/mcp/tool-schemas.js +28 -2
  52. package/package.json +4 -2
  53. package/.onto/domains/llm-native-development/competency_qs.md +0 -430
  54. package/.onto/domains/llm-native-development/concepts.md +0 -242
  55. package/.onto/domains/llm-native-development/conciseness_rules.md +0 -163
  56. package/.onto/domains/llm-native-development/dependency_rules.md +0 -216
  57. package/.onto/domains/llm-native-development/domain_scope.md +0 -197
  58. package/.onto/domains/llm-native-development/extension_cases.md +0 -474
  59. package/.onto/domains/llm-native-development/logic_rules.md +0 -123
  60. package/.onto/domains/llm-native-development/prompt_interface.md +0 -49
  61. package/.onto/domains/llm-native-development/structure_spec.md +0 -245
@@ -11,14 +11,17 @@ import { executeReviewPromptExecution, } from "./run-review-prompt-execution.js"
11
11
  import { startReviewSession } from "./start-review-session.js";
12
12
  import { spawnWatcherPane } from "./spawn-watcher.js";
13
13
  import { generateReviewSessionId } from "../review/materializers.js";
14
- import { fileExists, hasOptionFlag, normalizeDomainValue, readMultiOptionValuesFromArgv, readYamlDocument, readSingleOptionValueFromArgv, } from "../review/review-artifact-utils.js";
14
+ import { fileExists, hasOptionFlag, isDeprecatedDomainAlias, normalizeDomainValue, readMultiOptionValuesFromArgv, readYamlDocument, readSingleOptionValueFromArgv, } from "../review/review-artifact-utils.js";
15
15
  import { printOntoReleaseChannelNotice } from "../release-channel/release-channel.js";
16
- import { resolveOntoHome } from "../discovery/onto-home.js";
16
+ import { isOntoRoot, resolveOntoHome } from "../discovery/onto-home.js";
17
+ import { resolveInstallationPath } from "../discovery/installation-paths.js";
17
18
  import { resolveSettingsChain } from "../discovery/settings-chain.js";
18
19
  import { loadCoreLensRegistry } from "../discovery/lens-registry.js";
20
+ import { isPathInsideRoot, isPathInsideRootRealpathAwareSync, } from "../path-boundary.js";
19
21
  import { normalizeLlmModelSwitcher } from "../llm/model-switcher.js";
20
22
  import { resolveReviewExecutionProfile, } from "../review/review-execution-profile.js";
21
23
  import { buildReviewExecutionRoute } from "../review/review-execution-route.js";
24
+ import { prepareReviewInvocationArgv, runReviewInvocationArgv, } from "../review/review-invocation-runner.js";
22
25
  import { readValidatedReviewRecord } from "../review/review-record-validation.js";
23
26
  import { readReviewResultClassification } from "../review/review-result-classification.js";
24
27
  import { createStructuredFailureRecord, ReviewStructuredFailureError, writeAndThrowStructuredFailureRecord, } from "../review/failure-records.js";
@@ -124,7 +127,7 @@ function resolveDirectExecutorPath(realization, ontoHome) {
124
127
  }
125
128
  return null;
126
129
  }
127
- function buildExecutorConfigFromRealization(realization, ontoHome) {
130
+ export function buildExecutorConfigFromRealization(realization, ontoHome) {
128
131
  if (typeof ontoHome === "string" && ontoHome.length > 0) {
129
132
  const direct = resolveDirectExecutorPath(realization, ontoHome);
130
133
  if (direct) {
@@ -139,7 +142,7 @@ function buildExecutorConfigFromRealization(realization, ontoHome) {
139
142
  }
140
143
  throw new Error("ontoHome is required to resolve review executor script paths.");
141
144
  }
142
- function inferExecutorRealization(config) {
145
+ export function inferExecutorRealization(config) {
143
146
  const joinedArgs = config.args.join(" ");
144
147
  for (const [realization, filename] of Object.entries(EXECUTOR_SCRIPT_FILENAMES)) {
145
148
  if (joinedArgs.includes(filename)) {
@@ -179,10 +182,6 @@ function applyExecutorOverrideToProfile(profile, argv) {
179
182
  }
180
183
  return profile;
181
184
  }
182
- function isInsidePath(root, candidate) {
183
- const relative = path.relative(path.resolve(root), path.resolve(candidate));
184
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
185
- }
186
185
  function displayPathFromProject(projectRoot, candidate) {
187
186
  if (!path.isAbsolute(candidate))
188
187
  return candidate;
@@ -217,7 +216,7 @@ function renderReviewStartPreview(args) {
217
216
  const { projectRoot, sessionRoot, setup, reviewExecutionProfile } = args;
218
217
  const inputs = setup.resolvedInvokeInputs;
219
218
  const targetPath = path.resolve(inputs.targetPath);
220
- const boundaryLabel = isInsidePath(projectRoot, targetPath)
219
+ const boundaryLabel = isPathInsideRoot(projectRoot, targetPath)
221
220
  ? "project"
222
221
  : "external";
223
222
  const projectSettingsPath = path.join(projectRoot, ".onto", "settings.json");
@@ -246,6 +245,7 @@ function renderReviewStartPreview(args) {
246
245
  ` selected: ${selectedDomain}`,
247
246
  ` selection_mode: ${inputs.domainSelectionMode}`,
248
247
  ` selection_required: ${String(inputs.domainSelectionRequired)}`,
248
+ ` selection_reason: ${inputs.domainSelectionReason}`,
249
249
  "review_lenses:",
250
250
  ` review_mode: ${inputs.reviewMode}`,
251
251
  ` lens_count: ${configuredLensIds.length}`,
@@ -337,7 +337,7 @@ function renderScreenBoundedLines(text, maxLines = 10) {
337
337
  }
338
338
  return bounded;
339
339
  }
340
- async function readReviewResultExplanationSummary(finalOutputPath) {
340
+ export async function readReviewResultExplanationSummary(finalOutputPath) {
341
341
  if (!(await fileExists(finalOutputPath))) {
342
342
  const unavailable = "- final output unavailable";
343
343
  return {
@@ -357,7 +357,7 @@ async function readReviewResultExplanationSummary(finalOutputPath) {
357
357
  screen_lines: renderScreenBoundedLines(finalReviewResult),
358
358
  };
359
359
  }
360
- async function readReviewResultClosureSummary(sessionRoot) {
360
+ export async function readReviewResultClosureSummary(sessionRoot) {
361
361
  const problemFramingPath = path.join(sessionRoot, "problem-framing.yaml");
362
362
  const resultClassification = await readReviewResultClassification(sessionRoot);
363
363
  const problemFraming = (await fileExists(problemFramingPath))
@@ -407,6 +407,7 @@ function renderReviewResultOverview(args) {
407
407
  ` target: ${args.target}`,
408
408
  ` target_scope_kind: ${args.targetScopeKind}`,
409
409
  ` domain: ${args.domain.length > 0 ? args.domain : "none"}`,
410
+ ` domain_selection_reason: ${args.domainSelectionReason}`,
410
411
  "coverage:",
411
412
  ` lenses: ${args.participatingLensIds.length}/${args.plannedLensIds.length} participating`,
412
413
  ` degraded_lenses: ${degraded}`,
@@ -613,7 +614,7 @@ function buildNoHostDetectedError() {
613
614
  " 4. 테스트 실행은 --executor-realization mock 사용",
614
615
  ].join("\n"));
615
616
  }
616
- function resolveExecutorConfig(argv, optionPrefix, ontoConfig, ontoHome, reviewExecutionProfile, actor = "lens") {
617
+ export function resolveExecutorConfig(argv, optionPrefix, ontoConfig, ontoHome, reviewExecutionProfile, actor = "lens") {
617
618
  const optionPrefixLabel = optionPrefix.length > 0 ? optionPrefix : "";
618
619
  const actorLlmRef = reviewExecutionProfile?.[actor].llm;
619
620
  const explicitBin = readSingleOptionValueFromArgv(argv, `${optionPrefixLabel}executor-bin`);
@@ -684,7 +685,7 @@ function assertValidLocalBaseUrl(baseUrl) {
684
685
  throw new Error(`Invalid local provider base_url: ${baseUrl}`);
685
686
  }
686
687
  }
687
- async function ensureProviderRouteReadyForDispatch(args) {
688
+ export async function ensureProviderRouteReadyForDispatch(args) {
688
689
  const profile = args.reviewExecutionProfile;
689
690
  if (profile.worker_executor === "mock") {
690
691
  return;
@@ -855,26 +856,6 @@ function parseHostFacingPositionals(positionals) {
855
856
  intentText: [second, ...rest].filter((value) => typeof value === "string").join(" ").trim(),
856
857
  };
857
858
  }
858
- function isPathInsideRoot(candidatePath, rootPath) {
859
- let resolvedCandidate;
860
- let resolvedRoot;
861
- try {
862
- resolvedCandidate = fsSync.realpathSync(candidatePath);
863
- resolvedRoot = fsSync.realpathSync(rootPath);
864
- }
865
- catch {
866
- resolvedCandidate = path.resolve(candidatePath);
867
- resolvedRoot = path.resolve(rootPath);
868
- }
869
- const relative = path.relative(resolvedRoot, resolvedCandidate);
870
- if (relative === "") {
871
- return true;
872
- }
873
- if (relative.startsWith("..")) {
874
- return false;
875
- }
876
- return !path.isAbsolute(relative);
877
- }
878
859
  function normalizeFilesystemAllowedRoot(root, defaultProjectRoot) {
879
860
  if (path.isAbsolute(root)) {
880
861
  return path.resolve(root);
@@ -894,7 +875,7 @@ function normalizeFilesystemAllowedRoots(filesystemAllowedRoots, defaultProjectR
894
875
  return deduped;
895
876
  }
896
877
  function isInsideAnyDeclaredFilesystemRoot(targetPath, allowedRoots) {
897
- return allowedRoots.some((allowedRoot) => isPathInsideRoot(targetPath, allowedRoot));
878
+ return allowedRoots.some((allowedRoot) => isPathInsideRootRealpathAwareSync(allowedRoot, targetPath));
898
879
  }
899
880
  function deriveFilesystemBoundaryFromTarget(targetPath, targetScopeKind) {
900
881
  return targetScopeKind === "file"
@@ -960,10 +941,11 @@ function normalizeDomainToken(domainValue) {
960
941
  if (trimmed.length === 0) {
961
942
  return null;
962
943
  }
963
- if (["-", "@-", "none"].includes(trimmed)) {
944
+ const normalized = normalizeDomainValue(trimmed);
945
+ if (normalized === "none") {
964
946
  return "@-";
965
947
  }
966
- return trimmed.startsWith("@") ? trimmed : `@${trimmed}`;
948
+ return `@${normalized}`;
967
949
  }
968
950
  function collectConfiguredDomainTokens(ontoConfig) {
969
951
  const collected = [];
@@ -996,6 +978,313 @@ function collectConfiguredDomainTokens(ontoConfig) {
996
978
  pushTokenList(ontoConfig.domains);
997
979
  return collected;
998
980
  }
981
+ const DOMAIN_INFERENCE_MAX_FILE_BYTES = 64 * 1024;
982
+ const DOMAIN_INFERENCE_MAX_DIRECTORY_ENTRIES = 200;
983
+ const DOMAIN_INFERENCE_MAX_DIRECTORY_DEPTH = 3;
984
+ const DOMAIN_INFERENCE_MIN_SCORE = 18;
985
+ const DOMAIN_INFERENCE_EXCLUDED_NAMES = new Set([
986
+ ".git",
987
+ "node_modules",
988
+ ".onto/review",
989
+ "dist",
990
+ "build",
991
+ ".next",
992
+ "coverage",
993
+ ]);
994
+ const DOMAIN_INFERENCE_CODE_EXTENSIONS = new Set([
995
+ ".c",
996
+ ".cc",
997
+ ".cpp",
998
+ ".cs",
999
+ ".go",
1000
+ ".java",
1001
+ ".js",
1002
+ ".jsx",
1003
+ ".kt",
1004
+ ".mjs",
1005
+ ".py",
1006
+ ".rs",
1007
+ ".swift",
1008
+ ".ts",
1009
+ ".tsx",
1010
+ ]);
1011
+ const DOMAIN_INFERENCE_STOPWORDS = new Set([
1012
+ "a",
1013
+ "an",
1014
+ "and",
1015
+ "are",
1016
+ "as",
1017
+ "be",
1018
+ "by",
1019
+ "can",
1020
+ "code",
1021
+ "content",
1022
+ "data",
1023
+ "doc",
1024
+ "document",
1025
+ "domain",
1026
+ "file",
1027
+ "for",
1028
+ "from",
1029
+ "has",
1030
+ "in",
1031
+ "is",
1032
+ "it",
1033
+ "of",
1034
+ "on",
1035
+ "or",
1036
+ "review",
1037
+ "rule",
1038
+ "should",
1039
+ "system",
1040
+ "target",
1041
+ "that",
1042
+ "the",
1043
+ "this",
1044
+ "to",
1045
+ "with",
1046
+ ]);
1047
+ function addDomainRootCandidates(args) {
1048
+ if (!fsSync.existsSync(args.root))
1049
+ return;
1050
+ let entries;
1051
+ try {
1052
+ entries = fsSync.readdirSync(args.root, { withFileTypes: true });
1053
+ }
1054
+ catch {
1055
+ return;
1056
+ }
1057
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1058
+ for (const entry of entries) {
1059
+ if (!entry.isDirectory() || entry.name.startsWith("."))
1060
+ continue;
1061
+ if (isDeprecatedDomainAlias(entry.name))
1062
+ continue;
1063
+ if (args.seenIds.has(entry.name))
1064
+ continue;
1065
+ args.seenIds.add(entry.name);
1066
+ args.candidates.push({
1067
+ id: entry.name,
1068
+ dir: path.join(args.root, entry.name),
1069
+ source: args.source,
1070
+ });
1071
+ }
1072
+ }
1073
+ function collectAvailableDomainCandidates(projectRoot, ontoHome) {
1074
+ const candidates = [];
1075
+ const seenIds = new Set();
1076
+ const seenRoots = new Set();
1077
+ const addRoot = (root, source) => {
1078
+ const resolvedRoot = path.resolve(root);
1079
+ if (seenRoots.has(resolvedRoot))
1080
+ return;
1081
+ seenRoots.add(resolvedRoot);
1082
+ addDomainRootCandidates({ candidates, seenIds, root: resolvedRoot, source });
1083
+ };
1084
+ addRoot(path.join(projectRoot, ".onto", "domains"), "project");
1085
+ addRoot(path.join(os.homedir(), ".onto", "domains"), "user");
1086
+ if (typeof ontoHome === "string" && ontoHome.length > 0) {
1087
+ try {
1088
+ addRoot(resolveInstallationPath("domains", ontoHome), "installation");
1089
+ }
1090
+ catch {
1091
+ // No installation domain seat available.
1092
+ }
1093
+ }
1094
+ if (isOntoRoot(projectRoot)) {
1095
+ try {
1096
+ addRoot(resolveInstallationPath("domains", projectRoot), "dev_install");
1097
+ }
1098
+ catch {
1099
+ // Not a canonical install domain seat.
1100
+ }
1101
+ }
1102
+ return candidates;
1103
+ }
1104
+ function tokenizeForDomainInference(text) {
1105
+ return text
1106
+ .toLowerCase()
1107
+ .replace(/[-_/.:]+/g, " ")
1108
+ .split(/[^\p{L}\p{N}]+/u)
1109
+ .map((token) => token.trim())
1110
+ .filter((token) => token.length >= 3 &&
1111
+ !DOMAIN_INFERENCE_STOPWORDS.has(token) &&
1112
+ !/^\d+$/.test(token));
1113
+ }
1114
+ async function readFilePrefix(filePath, maxBytes) {
1115
+ let handle;
1116
+ try {
1117
+ const stats = await fs.stat(filePath);
1118
+ const byteLength = Math.min(stats.size, maxBytes);
1119
+ if (byteLength <= 0)
1120
+ return "";
1121
+ handle = await fs.open(filePath, "r");
1122
+ const buffer = Buffer.alloc(byteLength);
1123
+ const { bytesRead } = await handle.read(buffer, 0, byteLength, 0);
1124
+ return buffer.subarray(0, bytesRead).toString("utf8");
1125
+ }
1126
+ catch {
1127
+ return "";
1128
+ }
1129
+ finally {
1130
+ await handle?.close();
1131
+ }
1132
+ }
1133
+ async function collectDirectorySignalPaths(root, maxEntries = DOMAIN_INFERENCE_MAX_DIRECTORY_ENTRIES, maxDepth = DOMAIN_INFERENCE_MAX_DIRECTORY_DEPTH) {
1134
+ const collected = [];
1135
+ async function walk(current, depth) {
1136
+ if (collected.length >= maxEntries || depth > maxDepth)
1137
+ return;
1138
+ let entries;
1139
+ try {
1140
+ entries = await fs.readdir(current, { withFileTypes: true });
1141
+ }
1142
+ catch {
1143
+ return;
1144
+ }
1145
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1146
+ for (const entry of entries) {
1147
+ if (collected.length >= maxEntries)
1148
+ return;
1149
+ const relative = path.relative(root, path.join(current, entry.name));
1150
+ const normalizedRelative = relative.split(path.sep).join(path.posix.sep);
1151
+ if (DOMAIN_INFERENCE_EXCLUDED_NAMES.has(entry.name) ||
1152
+ DOMAIN_INFERENCE_EXCLUDED_NAMES.has(normalizedRelative)) {
1153
+ continue;
1154
+ }
1155
+ if (entry.isDirectory()) {
1156
+ await walk(path.join(current, entry.name), depth + 1);
1157
+ continue;
1158
+ }
1159
+ if (entry.isFile()) {
1160
+ collected.push(normalizedRelative);
1161
+ }
1162
+ }
1163
+ }
1164
+ await walk(root, 0);
1165
+ return collected;
1166
+ }
1167
+ async function buildTargetDomainSignal(args) {
1168
+ const pathParts = [
1169
+ args.requestedTarget,
1170
+ path.relative(args.projectRoot, args.targetPath),
1171
+ ...args.resolvedTargetRefs.map((ref) => path.relative(args.projectRoot, ref)),
1172
+ ].map((part) => part.split(path.sep).join(path.posix.sep));
1173
+ const contentParts = [args.requestText, ...pathParts];
1174
+ let hasCodeSignal = false;
1175
+ let hasOntologyPathSignal = pathParts.some((part) => part === ".onto" ||
1176
+ part.startsWith(".onto/") ||
1177
+ part.includes("/ontology/") ||
1178
+ part.includes("ontology"));
1179
+ for (const ref of args.resolvedTargetRefs) {
1180
+ try {
1181
+ const stats = await fs.stat(ref);
1182
+ if (stats.isDirectory()) {
1183
+ const listedPaths = await collectDirectorySignalPaths(ref);
1184
+ contentParts.push(...listedPaths);
1185
+ hasCodeSignal ||= listedPaths.some((listedPath) => DOMAIN_INFERENCE_CODE_EXTENSIONS.has(path.extname(listedPath)));
1186
+ hasOntologyPathSignal ||= listedPaths.some((listedPath) => listedPath.startsWith(".onto/") ||
1187
+ listedPath.includes("/ontology/") ||
1188
+ listedPath.includes("ontology"));
1189
+ continue;
1190
+ }
1191
+ hasCodeSignal ||= DOMAIN_INFERENCE_CODE_EXTENSIONS.has(path.extname(ref));
1192
+ contentParts.push(await readFilePrefix(ref, DOMAIN_INFERENCE_MAX_FILE_BYTES));
1193
+ }
1194
+ catch {
1195
+ // Target binding already validated refs; inference remains best-effort.
1196
+ }
1197
+ }
1198
+ return {
1199
+ text: contentParts.join("\n"),
1200
+ pathText: pathParts.join("\n"),
1201
+ hasCodeSignal,
1202
+ hasOntologyPathSignal,
1203
+ };
1204
+ }
1205
+ async function readDomainCandidateText(candidate) {
1206
+ const preferredFiles = [
1207
+ "domain_scope.md",
1208
+ "concepts.md",
1209
+ "competency_qs.md",
1210
+ "structure_spec.md",
1211
+ "logic_rules.md",
1212
+ ];
1213
+ const parts = [candidate.id, candidate.id.replace(/-/g, " ")];
1214
+ for (const filename of preferredFiles) {
1215
+ const filePath = path.join(candidate.dir, filename);
1216
+ if (!fsSync.existsSync(filePath))
1217
+ continue;
1218
+ parts.push(await readFilePrefix(filePath, 24 * 1024));
1219
+ }
1220
+ return parts.join("\n");
1221
+ }
1222
+ function formatInferenceReason(args) {
1223
+ const reason = args.signalReasons.length > 0
1224
+ ? args.signalReasons.join("; ")
1225
+ : "it had the strongest lexical overlap with the target and intent";
1226
+ return `No explicit domain token or configured domain was provided. Selected ${args.domainToken} because ${reason}.`;
1227
+ }
1228
+ async function inferDomainFromReviewTarget(args) {
1229
+ const candidates = collectAvailableDomainCandidates(args.projectRoot, args.ontoHome);
1230
+ if (candidates.length === 0)
1231
+ return null;
1232
+ const signal = await buildTargetDomainSignal(args);
1233
+ const targetTokens = new Set(tokenizeForDomainInference(signal.text));
1234
+ const pathTokens = new Set(tokenizeForDomainInference(signal.pathText));
1235
+ const normalizedTargetText = signal.text.toLowerCase();
1236
+ const scored = await Promise.all(candidates.map(async (candidate, index) => {
1237
+ let score = 0;
1238
+ const signalReasons = [];
1239
+ const domainToken = `@${candidate.id}`;
1240
+ const domainIdTokens = tokenizeForDomainInference(candidate.id);
1241
+ if (normalizedTargetText.includes(candidate.id.toLowerCase())) {
1242
+ score += 80;
1243
+ signalReasons.push(`the target mentions "${candidate.id}"`);
1244
+ }
1245
+ const matchedIdTokens = domainIdTokens.filter((token) => targetTokens.has(token) || pathTokens.has(token));
1246
+ if (matchedIdTokens.length > 0) {
1247
+ score += matchedIdTokens.length * 30;
1248
+ signalReasons.push(`target path or intent matches domain token(s): ${matchedIdTokens.join(", ")}`);
1249
+ }
1250
+ if (candidate.id === "ontology" && signal.hasOntologyPathSignal) {
1251
+ score += 55;
1252
+ signalReasons.push("the target is inside or explicitly references ontology-owned material");
1253
+ }
1254
+ if (candidate.id === "software-engineering" && signal.hasCodeSignal) {
1255
+ score += 45;
1256
+ signalReasons.push("the target contains implementation/code material");
1257
+ }
1258
+ const domainText = await readDomainCandidateText(candidate);
1259
+ const domainTokens = new Set(tokenizeForDomainInference(domainText));
1260
+ const overlap = [...targetTokens].filter((token) => domainTokens.has(token));
1261
+ if (overlap.length > 0) {
1262
+ const weightedOverlap = Math.min(overlap.length * 2, 40);
1263
+ score += weightedOverlap;
1264
+ signalReasons.push(`target text overlaps domain document terms: ${overlap.slice(0, 5).join(", ")}`);
1265
+ }
1266
+ return {
1267
+ candidate,
1268
+ domainToken,
1269
+ score,
1270
+ index,
1271
+ signalReasons,
1272
+ };
1273
+ }));
1274
+ scored.sort((left, right) => right.score - left.score ||
1275
+ left.index - right.index ||
1276
+ left.candidate.id.localeCompare(right.candidate.id));
1277
+ const best = scored[0];
1278
+ if (!best || best.score < DOMAIN_INFERENCE_MIN_SCORE)
1279
+ return null;
1280
+ return {
1281
+ domainToken: best.domainToken,
1282
+ reason: formatInferenceReason({
1283
+ domainToken: best.domainToken,
1284
+ signalReasons: best.signalReasons,
1285
+ }),
1286
+ };
1287
+ }
999
1288
  async function promptForDomainSelection(configuredDomainTokens) {
1000
1289
  const optionTokens = configuredDomainTokens.includes("@-")
1001
1290
  ? [...configuredDomainTokens]
@@ -1035,31 +1324,54 @@ async function promptForDomainSelection(configuredDomainTokens) {
1035
1324
  readline.close();
1036
1325
  }
1037
1326
  }
1038
- async function resolveDomainSelection(requestedDomainToken, ontoConfig) {
1327
+ async function resolveDomainSelection(requestedDomainToken, ontoConfig, inferenceContext) {
1039
1328
  if (requestedDomainToken.length > 0) {
1329
+ const domainFinalValue = normalizeDomainValue(requestedDomainToken);
1330
+ const reason = domainFinalValue === "none"
1331
+ ? "Explicit no-domain token was provided; runtime will run without domain documents."
1332
+ : `Explicit domain token ${requestedDomainToken} was provided; runtime used it without target inference.`;
1040
1333
  return {
1041
1334
  domainRecommendation: requestedDomainToken,
1042
- domainFinalValue: normalizeDomainValue(requestedDomainToken),
1335
+ domainFinalValue,
1043
1336
  domainSelectionMode: "explicit_token",
1044
1337
  domainSelectionRequired: false,
1338
+ domainSelectionReason: reason,
1339
+ bindingNotes: [reason],
1045
1340
  };
1046
1341
  }
1047
1342
  const configuredDomainTokens = collectConfiguredDomainTokens(ontoConfig);
1048
1343
  if (configuredDomainTokens.length === 0) {
1344
+ const inferred = await inferDomainFromReviewTarget(inferenceContext);
1345
+ if (inferred) {
1346
+ return {
1347
+ domainRecommendation: inferred.domainToken,
1348
+ domainFinalValue: normalizeDomainValue(inferred.domainToken),
1349
+ domainSelectionMode: "target_inferred",
1350
+ domainSelectionRequired: false,
1351
+ domainSelectionReason: inferred.reason,
1352
+ bindingNotes: [inferred.reason],
1353
+ };
1354
+ }
1355
+ const reason = "No explicit domain token, configured domain, or confident target-domain match was available; runtime will run without domain documents.";
1049
1356
  return {
1050
1357
  domainRecommendation: "@-",
1051
1358
  domainFinalValue: "none",
1052
1359
  domainSelectionMode: "no_domain_default",
1053
1360
  domainSelectionRequired: false,
1361
+ domainSelectionReason: reason,
1362
+ bindingNotes: [reason],
1054
1363
  };
1055
1364
  }
1056
1365
  if (configuredDomainTokens.length === 1) {
1057
1366
  const selectedToken = configuredDomainTokens[0];
1367
+ const reason = `Single configured domain ${selectedToken} is available; runtime used the configured default.`;
1058
1368
  return {
1059
1369
  domainRecommendation: selectedToken,
1060
1370
  domainFinalValue: normalizeDomainValue(selectedToken),
1061
1371
  domainSelectionMode: "project_default",
1062
1372
  domainSelectionRequired: false,
1373
+ domainSelectionReason: reason,
1374
+ bindingNotes: [reason],
1063
1375
  };
1064
1376
  }
1065
1377
  const domainRecommendation = configuredDomainTokens[0];
@@ -1071,11 +1383,14 @@ async function resolveDomainSelection(requestedDomainToken, ontoConfig) {
1071
1383
  ].join("\n"));
1072
1384
  }
1073
1385
  const selectedToken = await promptForDomainSelection(configuredDomainTokens);
1386
+ const reason = `Interactive domain selection chose ${selectedToken} from configured domains: ${configuredDomainTokens.join(", ")}.`;
1074
1387
  return {
1075
1388
  domainRecommendation,
1076
1389
  domainFinalValue: normalizeDomainValue(selectedToken),
1077
1390
  domainSelectionMode: "interactive_selection",
1078
1391
  domainSelectionRequired: true,
1392
+ domainSelectionReason: reason,
1393
+ bindingNotes: [reason],
1079
1394
  };
1080
1395
  }
1081
1396
  function resolveReviewMode(argv, ontoConfig) {
@@ -1268,7 +1583,7 @@ async function resolveBundleTargetInput(args) {
1268
1583
  filesystemAllowedRoots,
1269
1584
  };
1270
1585
  }
1271
- async function resolveReviewInvokeInputs(argv, ontoConfig, projectRoot, sessionId) {
1586
+ async function resolveReviewInvokeInputs(argv, ontoConfig, projectRoot, sessionId, ontoHome) {
1272
1587
  const parsedPositionals = parseHostFacingPositionals(splitArgvIntoOptionsAndPositionals(argv, [...KNOWN_INVOKE_ONLY_OPTION_NAMES, ...KNOWN_PASSTHROUGH_OPTION_NAMES], [...KNOWN_INVOKE_ONLY_FLAG_NAMES, ...KNOWN_PASSTHROUGH_FLAG_NAMES]).positionals);
1273
1588
  const explicitRequestedTarget = readSingleOptionValueFromArgv(argv, "requested-target");
1274
1589
  const explicitTargetScopeKind = requireOptionalTargetScopeKind(readSingleOptionValueFromArgv(argv, "target-scope-kind"));
@@ -1347,7 +1662,6 @@ async function resolveReviewInvokeInputs(argv, ontoConfig, projectRoot, sessionI
1347
1662
  const requestedDomainToken = readSingleOptionValueFromArgv(argv, "requested-domain-token") ??
1348
1663
  (canonicalDomainToken.length > 0 ? canonicalDomainToken : undefined) ??
1349
1664
  "";
1350
- const resolvedDomainSelection = await resolveDomainSelection(requestedDomainToken, ontoConfig);
1351
1665
  let reviewMode = resolveReviewMode(argv, ontoConfig);
1352
1666
  const explicitLensIds = readMultiOptionValuesFromArgv(argv, "lens-id");
1353
1667
  // Phase 3: standalone LLM-based complexity assessment (Step 1.5)
@@ -1497,6 +1811,15 @@ async function resolveReviewInvokeInputs(argv, ontoConfig, projectRoot, sessionI
1497
1811
  if (resolvedLensIds.length === 0) {
1498
1812
  throw new Error("No lens IDs resolved. Specify at least one --lens-id or use --review-mode full|core-axis.");
1499
1813
  }
1814
+ const resolvedDomainSelection = await resolveDomainSelection(requestedDomainToken, ontoConfig, {
1815
+ projectRoot,
1816
+ ...(ontoHome ? { ontoHome } : {}),
1817
+ requestedTarget: requestedTarget ?? explicitPrimaryRef ?? absoluteTargetPath,
1818
+ requestText,
1819
+ targetPath: absoluteTargetPath,
1820
+ targetScopeKind,
1821
+ resolvedTargetRefs,
1822
+ });
1500
1823
  return {
1501
1824
  requestedTarget: requestedTarget ?? explicitPrimaryRef ?? absoluteTargetPath,
1502
1825
  targetPath: absoluteTargetPath,
@@ -1509,6 +1832,8 @@ async function resolveReviewInvokeInputs(argv, ontoConfig, projectRoot, sessionI
1509
1832
  domainFinalValue: resolvedDomainSelection.domainFinalValue,
1510
1833
  domainSelectionMode: resolvedDomainSelection.domainSelectionMode,
1511
1834
  domainSelectionRequired: resolvedDomainSelection.domainSelectionRequired,
1835
+ domainSelectionReason: resolvedDomainSelection.domainSelectionReason,
1836
+ bindingNotes: resolvedDomainSelection.bindingNotes,
1512
1837
  ...(bundleKind ? { bundleKind } : {}),
1513
1838
  reviewMode,
1514
1839
  reviewModeRecommendation: reviewMode,
@@ -1537,6 +1862,15 @@ function appendReviewInvokeDerivedArgs(argv, resolvedInputs) {
1537
1862
  appended.push(`--${optionName}`, value);
1538
1863
  }
1539
1864
  };
1865
+ const appendMissingMulti = (optionName, values) => {
1866
+ const existingValues = new Set(readMultiOptionValuesFromArgv(appended, optionName));
1867
+ for (const value of values) {
1868
+ if (existingValues.has(value))
1869
+ continue;
1870
+ appended.push(`--${optionName}`, value);
1871
+ existingValues.add(value);
1872
+ }
1873
+ };
1540
1874
  appendSingleIfAbsent("requested-target", resolvedInputs.requestedTarget);
1541
1875
  appendSingleIfAbsent("target-scope-kind", resolvedInputs.targetScopeKind);
1542
1876
  appendSingleIfAbsent("primary-ref", resolvedInputs.targetPath);
@@ -1555,6 +1889,7 @@ function appendReviewInvokeDerivedArgs(argv, resolvedInputs) {
1555
1889
  appendMultiIfAbsent("filesystem-allowed-root", resolvedInputs.filesystemAllowedRoots);
1556
1890
  appendMultiIfAbsent("lens-id", resolvedInputs.resolvedLensIds);
1557
1891
  appendMultiIfAbsent("materialized-ref", resolvedInputs.resolvedTargetRefs);
1892
+ appendMissingMulti("binding-note", resolvedInputs.bindingNotes);
1558
1893
  if (resolvedInputs.targetScopeKind === "bundle") {
1559
1894
  appendMultiIfAbsent("member-ref", resolvedInputs.resolvedTargetRefs.slice(1));
1560
1895
  if (typeof resolvedInputs.bundleKind === "string" &&
@@ -1569,7 +1904,7 @@ function appendReviewInvokeDerivedArgs(argv, resolvedInputs) {
1569
1904
  }
1570
1905
  return appended;
1571
1906
  }
1572
- async function readOptionalReviewSummary(sessionRoot) {
1907
+ export async function readOptionalReviewSummary(sessionRoot) {
1573
1908
  const bindingPath = path.join(sessionRoot, "binding.yaml");
1574
1909
  const reviewRecordPath = path.join(sessionRoot, "review-record.yaml");
1575
1910
  const binding = (await fileExists(bindingPath))
@@ -1657,7 +1992,7 @@ function appendDirectoryListingConfigArgs(targetArgv, originalArgv, ontoConfig)
1657
1992
  }
1658
1993
  return result;
1659
1994
  }
1660
- async function resolveReviewInvokeSetup(argv) {
1995
+ export async function resolveReviewInvokeSetup(argv) {
1661
1996
  rejectRemovedFlags(argv);
1662
1997
  const argvWithSessionId = ensureSessionIdArg(argv);
1663
1998
  const sessionId = requireString(readSingleOptionValueFromArgv(argvWithSessionId, "session-id"), "session-id");
@@ -1667,7 +2002,7 @@ async function resolveReviewInvokeSetup(argv) {
1667
2002
  const ontoConfig = ontoHome
1668
2003
  ? await resolveSettingsChain(ontoHome, projectRoot)
1669
2004
  : await readOntoConfig(projectRoot);
1670
- const resolvedInvokeInputs = await resolveReviewInvokeInputs(argvWithSessionId, ontoConfig, projectRoot, sessionId);
2005
+ const resolvedInvokeInputs = await resolveReviewInvokeInputs(argvWithSessionId, ontoConfig, projectRoot, sessionId, ontoHome);
1671
2006
  const maxConcurrentLenses = Math.max(1, resolvedInvokeInputs.resolvedLensIds.length);
1672
2007
  const { optionTokens: argvWithoutPositionals } = splitArgvIntoOptionsAndPositionals(argvWithSessionId, [...KNOWN_INVOKE_ONLY_OPTION_NAMES, ...KNOWN_PASSTHROUGH_OPTION_NAMES], [...KNOWN_INVOKE_ONLY_FLAG_NAMES, ...KNOWN_PASSTHROUGH_FLAG_NAMES]);
1673
2008
  const normalizedStartArgv = appendReviewInvokeDerivedArgs(stripOptionsFromArgv(argvWithoutPositionals, [...KNOWN_INVOKE_ONLY_OPTION_NAMES], [...KNOWN_INVOKE_ONLY_FLAG_NAMES]), resolvedInvokeInputs);
@@ -1708,21 +2043,15 @@ async function resolveReviewInvokeSetup(argv) {
1708
2043
  * values written into the prepared session artifacts.
1709
2044
  */
1710
2045
  export async function reviewPrepareOnly(argv) {
1711
- const setup = await resolveReviewInvokeSetup(argv);
1712
- const startResult = await startReviewSession(setup.startArgv);
1713
- const sessionRoot = path.resolve(startResult.session_root);
1714
- const profile = setup.executionProfile;
1715
- return {
1716
- prepare_only: true,
1717
- session_root: sessionRoot,
1718
- request_text: setup.resolvedInvokeInputs.requestText,
1719
- execution_realization: profile.execution_realization,
1720
- host_runtime: profile.host_runtime,
1721
- review_mode: setup.resolvedInvokeInputs.reviewMode,
1722
- };
2046
+ return prepareReviewInvocationArgv(argv);
1723
2047
  }
1724
2048
  export async function runReviewInvokeCli(argv) {
1725
2049
  const prepareOnly = hasOptionFlag(argv, "prepare-only");
2050
+ if (!prepareOnly) {
2051
+ const output = await runReviewInvocationArgv(argv);
2052
+ console.log(JSON.stringify(output, null, 2));
2053
+ return 0;
2054
+ }
1726
2055
  const setup = await resolveReviewInvokeSetup(argv);
1727
2056
  const resolvedProjectRoot = path.resolve(readSingleOptionValueFromArgv(setup.startArgv, "project-root") ?? ".");
1728
2057
  const rawOntoHome = readSingleOptionValueFromArgv(setup.startArgv, "onto-home");
@@ -1944,6 +2273,7 @@ export async function runReviewInvokeCli(argv) {
1944
2273
  target: setup.resolvedInvokeInputs.requestedTarget,
1945
2274
  target_scope_kind: setup.resolvedInvokeInputs.targetScopeKind,
1946
2275
  domain: setup.resolvedInvokeInputs.domainFinalValue,
2276
+ domain_selection_reason: setup.resolvedInvokeInputs.domainSelectionReason,
1947
2277
  },
1948
2278
  coverage: {
1949
2279
  planned_lens_count: setup.resolvedInvokeInputs.resolvedLensIds.length,
@@ -1963,6 +2293,7 @@ export async function runReviewInvokeCli(argv) {
1963
2293
  target: setup.resolvedInvokeInputs.requestedTarget,
1964
2294
  targetScopeKind: setup.resolvedInvokeInputs.targetScopeKind,
1965
2295
  domain: setup.resolvedInvokeInputs.domainFinalValue,
2296
+ domainSelectionReason: setup.resolvedInvokeInputs.domainSelectionReason,
1966
2297
  status: recordStatus,
1967
2298
  deliberationStatus,
1968
2299
  reviewMode: setup.resolvedInvokeInputs.reviewMode,
@@ -1987,6 +2318,7 @@ export async function runReviewInvokeCli(argv) {
1987
2318
  : null,
1988
2319
  domain_selection_required: setup.resolvedInvokeInputs.domainSelectionRequired,
1989
2320
  domain_selection_mode: setup.resolvedInvokeInputs.domainSelectionMode,
2321
+ domain_selection_reason: setup.resolvedInvokeInputs.domainSelectionReason,
1990
2322
  domain_final_value: setup.resolvedInvokeInputs.domainFinalValue,
1991
2323
  review_mode: setup.resolvedInvokeInputs.reviewMode,
1992
2324
  },