@topogram/cli 0.3.64 → 0.3.65

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 (245) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/agent-ops/query-builders/auth.js +375 -0
  5. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  6. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  7. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  8. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  9. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  10. package/src/agent-ops/query-builders/change-risk.js +25 -0
  11. package/src/agent-ops/query-builders/common.js +149 -0
  12. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  13. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  14. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  15. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  16. package/src/agent-ops/query-builders/work-packets.js +417 -0
  17. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  18. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  19. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  20. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  21. package/src/agent-ops/query-builders.d.ts +26 -26
  22. package/src/agent-ops/query-builders.js +42 -5021
  23. package/src/catalog/constants.js +10 -0
  24. package/src/catalog/copy.js +60 -0
  25. package/src/catalog/diagnostics.js +15 -0
  26. package/src/catalog/entries.js +42 -0
  27. package/src/catalog/files.js +67 -0
  28. package/src/catalog/provenance.js +122 -0
  29. package/src/catalog/source.js +150 -0
  30. package/src/catalog/validation.js +252 -0
  31. package/src/catalog.d.ts +2 -0
  32. package/src/catalog.js +18 -746
  33. package/src/cli/commands/catalog/check.js +31 -0
  34. package/src/cli/commands/catalog/copy.js +59 -0
  35. package/src/cli/commands/catalog/doctor.js +248 -0
  36. package/src/cli/commands/catalog/help.js +21 -0
  37. package/src/cli/commands/catalog/list.js +52 -0
  38. package/src/cli/commands/catalog/runner.js +92 -0
  39. package/src/cli/commands/catalog/shared.js +17 -0
  40. package/src/cli/commands/catalog/show.js +134 -0
  41. package/src/cli/commands/catalog.js +30 -615
  42. package/src/cli/commands/generator-policy/package-info.js +162 -0
  43. package/src/cli/commands/generator-policy/payloads.js +372 -0
  44. package/src/cli/commands/generator-policy/printers.js +159 -0
  45. package/src/cli/commands/generator-policy/runner.js +81 -0
  46. package/src/cli/commands/generator-policy/shared.js +39 -0
  47. package/src/cli/commands/generator-policy.js +15 -783
  48. package/src/cli/commands/import/adopt.js +170 -0
  49. package/src/cli/commands/import/check.js +91 -0
  50. package/src/cli/commands/import/diff.js +84 -0
  51. package/src/cli/commands/import/help.js +47 -0
  52. package/src/cli/commands/import/paths.js +277 -0
  53. package/src/cli/commands/import/plan.js +284 -0
  54. package/src/cli/commands/import/refresh.js +470 -0
  55. package/src/cli/commands/import/status-history.js +196 -0
  56. package/src/cli/commands/import/workspace.js +230 -0
  57. package/src/cli/commands/import.js +33 -1732
  58. package/src/cli/commands/package/constants.js +17 -0
  59. package/src/cli/commands/package/doctor.js +240 -0
  60. package/src/cli/commands/package/help.js +27 -0
  61. package/src/cli/commands/package/lockfile.js +135 -0
  62. package/src/cli/commands/package/npm.js +97 -0
  63. package/src/cli/commands/package/reporting.js +35 -0
  64. package/src/cli/commands/package/runner.js +33 -0
  65. package/src/cli/commands/package/shared.js +9 -0
  66. package/src/cli/commands/package/update-cli.js +252 -0
  67. package/src/cli/commands/package/versions.js +35 -0
  68. package/src/cli/commands/package.js +29 -813
  69. package/src/cli/commands/query/change-plan.js +68 -0
  70. package/src/cli/commands/query/definitions.js +202 -0
  71. package/src/cli/commands/query/import-adopt.js +121 -0
  72. package/src/cli/commands/query/runner/artifacts.js +102 -0
  73. package/src/cli/commands/query/runner/boundaries.js +211 -0
  74. package/src/cli/commands/query/runner/change.js +182 -0
  75. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  76. package/src/cli/commands/query/runner/index.js +31 -0
  77. package/src/cli/commands/query/runner/output.js +12 -0
  78. package/src/cli/commands/query/runner/workflow.js +241 -0
  79. package/src/cli/commands/query/runner.js +3 -0
  80. package/src/cli/commands/query/workflow-context.js +5 -0
  81. package/src/cli/commands/query/workspace.js +274 -0
  82. package/src/cli/commands/query.js +9 -1300
  83. package/src/cli/commands/template/baseline.js +100 -0
  84. package/src/cli/commands/template/check.js +466 -0
  85. package/src/cli/commands/template/constants.js +8 -0
  86. package/src/cli/commands/template/diagnostics.js +26 -0
  87. package/src/cli/commands/template/help.js +28 -0
  88. package/src/cli/commands/template/lifecycle.js +404 -0
  89. package/src/cli/commands/template/list-show.js +287 -0
  90. package/src/cli/commands/template/policy.js +422 -0
  91. package/src/cli/commands/template/shared.js +127 -0
  92. package/src/cli/commands/template/updates.js +352 -0
  93. package/src/cli/commands/template.js +41 -2143
  94. package/src/generator/api/contracts.js +497 -0
  95. package/src/generator/api/metadata.js +221 -0
  96. package/src/generator/api/openapi.js +559 -0
  97. package/src/generator/api/schema.js +124 -0
  98. package/src/generator/api/types.d.ts +98 -0
  99. package/src/generator/api.js +3 -1195
  100. package/src/generator/context/shared/domain-sdlc.js +282 -0
  101. package/src/generator/context/shared/maintained-boundary.js +665 -0
  102. package/src/generator/context/shared/metrics.js +85 -0
  103. package/src/generator/context/shared/primitives.js +64 -0
  104. package/src/generator/context/shared/relationships.js +453 -0
  105. package/src/generator/context/shared/summaries.js +263 -0
  106. package/src/generator/context/shared/types.d.ts +207 -0
  107. package/src/generator/context/shared.d.ts +42 -0
  108. package/src/generator/context/shared.js +80 -1390
  109. package/src/generator/context/slice/core.js +397 -0
  110. package/src/generator/context/slice/sdlc.js +417 -0
  111. package/src/generator/context/slice/ui-packets.js +183 -0
  112. package/src/generator/context/slice.js +2 -859
  113. package/src/generator/registry/index.js +507 -0
  114. package/src/generator/registry.js +18 -504
  115. package/src/generator/runtime/environment/index.js +666 -0
  116. package/src/generator/runtime/environment.js +4 -666
  117. package/src/generator/runtime/runtime-check/index.js +554 -0
  118. package/src/generator/runtime/runtime-check.js +4 -554
  119. package/src/generator/runtime/shared/index.js +572 -0
  120. package/src/generator/runtime/shared.js +19 -570
  121. package/src/generator/shared.d.ts +2 -0
  122. package/src/generator/surfaces/shared.d.ts +3 -0
  123. package/src/generator/widget-conformance/behavior-report.js +258 -0
  124. package/src/generator/widget-conformance/checks.js +371 -0
  125. package/src/generator/widget-conformance/projection-context.js +200 -0
  126. package/src/generator/widget-conformance/report.js +166 -0
  127. package/src/generator/widget-conformance/types.d.ts +121 -0
  128. package/src/generator/widget-conformance.js +3 -824
  129. package/src/import/core/context.d.ts +3 -0
  130. package/src/import/core/contracts.d.ts +1 -0
  131. package/src/import/core/registry.d.ts +4 -0
  132. package/src/import/core/runner/candidates.js +217 -0
  133. package/src/import/core/runner/options.js +22 -0
  134. package/src/import/core/runner/reports.js +50 -0
  135. package/src/import/core/runner/run.js +79 -0
  136. package/src/import/core/runner/tracks.js +150 -0
  137. package/src/import/core/runner/ui-drafts.js +337 -0
  138. package/src/import/core/runner.js +3 -698
  139. package/src/import/core/shared/api-routes.js +221 -0
  140. package/src/import/core/shared/candidates.js +97 -0
  141. package/src/import/core/shared/files.js +177 -0
  142. package/src/import/core/shared/next-app.js +389 -0
  143. package/src/import/core/shared/types.d.ts +51 -0
  144. package/src/import/core/shared/ui-routes.js +230 -0
  145. package/src/import/core/shared.js +60 -861
  146. package/src/new-project/constants.js +128 -0
  147. package/src/new-project/create.js +83 -0
  148. package/src/new-project/json.js +28 -0
  149. package/src/new-project/metadata.js +96 -0
  150. package/src/new-project/package-spec.js +161 -0
  151. package/src/new-project/project-files.js +348 -0
  152. package/src/new-project/template-policy.js +269 -0
  153. package/src/new-project/template-resolution.js +368 -0
  154. package/src/new-project/template-snapshots.js +430 -0
  155. package/src/new-project/template-updates.js +512 -0
  156. package/src/new-project/types.d.ts +83 -0
  157. package/src/new-project.js +6 -2277
  158. package/src/parser.d.ts +87 -1
  159. package/src/parser.js +118 -0
  160. package/src/policy/review-boundaries.d.ts +15 -0
  161. package/src/project-config/index.js +564 -0
  162. package/src/project-config.js +19 -561
  163. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  164. package/src/resolver/enrich/bug.js +2 -0
  165. package/src/resolver/enrich/pitch.js +2 -0
  166. package/src/resolver/enrich/requirement.js +2 -0
  167. package/src/resolver/enrich/task.js +2 -0
  168. package/src/resolver/index.js +19 -2089
  169. package/src/resolver/normalize.js +384 -1
  170. package/src/resolver/plans.js +168 -0
  171. package/src/resolver/projections-api.js +494 -0
  172. package/src/resolver/projections-db.js +133 -0
  173. package/src/resolver/projections-ui.js +317 -0
  174. package/src/resolver/shapes.js +251 -0
  175. package/src/resolver/shared.js +278 -0
  176. package/src/resolver/widgets.js +132 -0
  177. package/src/template-trust/constants.js +62 -0
  178. package/src/template-trust/content.js +258 -0
  179. package/src/template-trust/diff.js +92 -0
  180. package/src/template-trust/policy.js +61 -0
  181. package/src/template-trust/record.js +90 -0
  182. package/src/template-trust/status.js +182 -0
  183. package/src/template-trust.js +24 -687
  184. package/src/text-helpers.d.ts +1 -0
  185. package/src/topogram-types.d.ts +69 -0
  186. package/src/validator/common.js +488 -0
  187. package/src/validator/data-model.js +237 -0
  188. package/src/validator/docs.js +167 -0
  189. package/src/validator/expressions.js +146 -1
  190. package/src/validator/index.d.ts +23 -0
  191. package/src/validator/index.js +32 -3585
  192. package/src/validator/kinds.d.ts +41 -0
  193. package/src/validator/kinds.js +2 -0
  194. package/src/validator/model-helpers.js +46 -0
  195. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  196. package/src/validator/per-kind/bug.js +6 -0
  197. package/src/validator/per-kind/domain.js +15 -2
  198. package/src/validator/per-kind/pitch.js +7 -0
  199. package/src/validator/per-kind/requirement.js +5 -0
  200. package/src/validator/per-kind/task.js +7 -0
  201. package/src/validator/per-kind/widget.js +14 -0
  202. package/src/validator/projections/api-http-async.js +410 -0
  203. package/src/validator/projections/api-http-authz.js +88 -0
  204. package/src/validator/projections/api-http-core.js +205 -0
  205. package/src/validator/projections/api-http-policies.js +339 -0
  206. package/src/validator/projections/api-http-responses.js +233 -0
  207. package/src/validator/projections/api-http.js +44 -0
  208. package/src/validator/projections/db.js +353 -0
  209. package/src/validator/projections/generator-defaults.js +45 -0
  210. package/src/validator/projections/helpers.js +87 -0
  211. package/src/validator/projections/ui-helpers.js +214 -0
  212. package/src/validator/projections/ui-navigation.js +344 -0
  213. package/src/validator/projections/ui-structure.js +364 -0
  214. package/src/validator/projections/ui-widgets.js +493 -0
  215. package/src/validator/projections/ui.js +46 -0
  216. package/src/validator/registry.js +48 -1
  217. package/src/validator/utils.d.ts +20 -0
  218. package/src/validator/utils.js +115 -12
  219. package/src/widget-behavior.d.ts +1 -0
  220. package/src/workflows/import-app/api/collect.js +221 -0
  221. package/src/workflows/import-app/api/openapi.js +257 -0
  222. package/src/workflows/import-app/api/routes.js +327 -0
  223. package/src/workflows/import-app/api/sources.js +22 -0
  224. package/src/workflows/import-app/api.js +2 -797
  225. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  226. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  227. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  228. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  229. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  230. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  231. package/src/workflows/reconcile/adoption-plan.js +30 -740
  232. package/src/workflows/reconcile/auth/closures.js +115 -0
  233. package/src/workflows/reconcile/auth/formatters.js +142 -0
  234. package/src/workflows/reconcile/auth/inference.js +330 -0
  235. package/src/workflows/reconcile/auth/roles.js +122 -0
  236. package/src/workflows/reconcile/auth.js +35 -690
  237. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  238. package/src/workflows/reconcile/bundle-core.js +12 -598
  239. package/src/workflows/reconcile/canonical-surface.js +1 -1
  240. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  241. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  242. package/src/workflows/reconcile/impacts/patches.js +252 -0
  243. package/src/workflows/reconcile/impacts/reports.js +80 -0
  244. package/src/workflows/reconcile/impacts.js +14 -623
  245. package/src/workspace-docs.d.ts +29 -0
@@ -0,0 +1,115 @@
1
+ // @ts-check
2
+
3
+ /** @param {any[]} items @returns {any} */
4
+ export function summarizeHintClosureState(items) {
5
+ const statuses = (items || []).map((/** @type {any} */ item) => item.status).filter(Boolean);
6
+ if (statuses.length === 0) {
7
+ return {
8
+ closure_state: "unresolved",
9
+ closure_reason: "No reviewed projection patch has been applied for this inferred auth hint yet."
10
+ };
11
+ }
12
+ if (statuses.every((/** @type {any} */ status) => status === "applied")) {
13
+ return {
14
+ closure_state: "adopted",
15
+ closure_reason: "All matching projection patch actions for this inferred auth hint have been applied."
16
+ };
17
+ }
18
+ if (statuses.every((/** @type {any} */ status) => ["applied", "approved", "skipped"].includes(status))) {
19
+ return {
20
+ closure_state: "deferred",
21
+ closure_reason: "This inferred auth hint has been reviewed or intentionally held back, but not every matching projection patch has been applied yet."
22
+ };
23
+ }
24
+ return {
25
+ closure_state: "unresolved",
26
+ closure_reason: "At least one matching projection patch for this inferred auth hint is still blocked on review or waiting to be applied."
27
+ };
28
+ }
29
+
30
+ /** @param {WorkflowRecord} bundle @param {any[]} planItems @returns {any} */
31
+ export function annotateBundleAuthHintClosures(bundle, planItems) {
32
+ const bundleItems = (planItems || []).filter((/** @type {any} */ item) => item.bundle === bundle.slug);
33
+ const annotatedPermissions = (bundle.authPermissionHints || []).map((/** @type {any} */ hint) => ({
34
+ ...hint,
35
+ ...summarizeHintClosureState(bundleItems.filter((/** @type {any} */ item) =>
36
+ item.suggested_action === "apply_projection_permission_patch" &&
37
+ item.permission === hint.permission
38
+ ))
39
+ }));
40
+ const annotatedClaims = (bundle.authClaimHints || []).map((/** @type {any} */ hint) => ({
41
+ ...hint,
42
+ ...summarizeHintClosureState(bundleItems.filter((/** @type {any} */ item) =>
43
+ item.suggested_action === "apply_projection_auth_patch" &&
44
+ item.claim === hint.claim &&
45
+ item.claim_value === (Object.prototype.hasOwnProperty.call(hint, "claim_value") ? hint.claim_value : null)
46
+ ))
47
+ }));
48
+ const annotatedOwnerships = (bundle.authOwnershipHints || []).map((/** @type {any} */ hint) => ({
49
+ ...hint,
50
+ ...summarizeHintClosureState(bundleItems.filter((/** @type {any} */ item) =>
51
+ item.suggested_action === "apply_projection_ownership_patch" &&
52
+ item.ownership === hint.ownership &&
53
+ item.ownership_field === hint.ownership_field
54
+ ))
55
+ }));
56
+ return {
57
+ ...bundle,
58
+ authPermissionHints: annotatedPermissions,
59
+ authClaimHints: annotatedClaims,
60
+ authOwnershipHints: annotatedOwnerships
61
+ };
62
+ }
63
+
64
+ /** @param {WorkflowRecord} bundle @returns {any} */
65
+ export function buildAuthHintClosureSummary(bundle) {
66
+ const hints = [
67
+ ...(bundle.authPermissionHints || []),
68
+ ...(bundle.authClaimHints || []),
69
+ ...(bundle.authOwnershipHints || [])
70
+ ];
71
+ const counts = hints.reduce(
72
+ (/** @type {any} */ acc, /** @type {any} */ hint) => {
73
+ const state = hint.closure_state || "unresolved";
74
+ if (state === "adopted") {
75
+ acc.adopted += 1;
76
+ } else if (state === "deferred") {
77
+ acc.deferred += 1;
78
+ } else {
79
+ acc.unresolved += 1;
80
+ }
81
+ return acc;
82
+ },
83
+ { total: hints.length, adopted: 0, deferred: 0, unresolved: 0 }
84
+ );
85
+ if (counts.total === 0) {
86
+ return {
87
+ status: "no_auth_hints",
88
+ label: "no auth hints",
89
+ reason: "This bundle does not currently carry inferred permission, claim, or ownership hints.",
90
+ ...counts
91
+ };
92
+ }
93
+ if (counts.unresolved === 0 && counts.deferred === 0) {
94
+ return {
95
+ status: "mostly_closed",
96
+ label: "mostly closed",
97
+ reason: "All inferred auth hints for this bundle have been adopted into canonical projection rules.",
98
+ ...counts
99
+ };
100
+ }
101
+ if (counts.unresolved === 0) {
102
+ return {
103
+ status: "partially_closed",
104
+ label: "partially closed",
105
+ reason: "Every inferred auth hint has been reviewed, but at least one is still intentionally deferred instead of adopted.",
106
+ ...counts
107
+ };
108
+ }
109
+ return {
110
+ status: "high_risk",
111
+ label: "high risk",
112
+ reason: "At least one inferred auth hint is still unresolved, so the recovered auth story for this bundle is not closed yet.",
113
+ ...counts
114
+ };
115
+ }
@@ -0,0 +1,142 @@
1
+ // @ts-check
2
+
3
+ /** @param {string} text @param {any[]} patterns @returns {any} */
4
+ export function authClaimPatternMatches(text, patterns = []) {
5
+ return patterns.some((/** @type {any} */ pattern) => pattern.test(text));
6
+ }
7
+
8
+ /** @param {any[]} entries @param {any[]} patterns @param {any} toText @returns {any} */
9
+ export function collectAuthClaimSignalMatches(entries, patterns, toText) {
10
+ return (entries || []).filter((/** @type {any} */ entry) => authClaimPatternMatches(toText(entry), patterns));
11
+ }
12
+
13
+ /** @param {string} value @returns {any} */
14
+ export function formatAuthClaimValueInline(value) {
15
+ return value == null ? "_dynamic_" : `\`${value}\``;
16
+ }
17
+
18
+ /** @param {WorkflowRecord} hint @returns {any} */
19
+ export function formatAuthClaimHintInline(hint) {
20
+ return `claim \`${hint.claim}\` = ${formatAuthClaimValueInline(hint.claim_value)} (${hint.confidence})`;
21
+ }
22
+
23
+ /** @param {WorkflowRecord} hint @returns {any} */
24
+ export function formatAuthPermissionHintInline(hint) {
25
+ return `permission \`${hint.permission}\` (${hint.confidence})`;
26
+ }
27
+
28
+ /** @param {WorkflowRecord} hint @returns {any} */
29
+ export function formatAuthOwnershipHintInline(hint) {
30
+ return `ownership \`${hint.ownership}\` field \`${hint.ownership_field}\` (${hint.confidence})`;
31
+ }
32
+
33
+ /** @param {WorkflowRecord} hint @returns {any} */
34
+ export function describeAuthPermissionWhyInferred(hint) {
35
+ /** @type {any[]} */
36
+ const signals = [];
37
+ if (hint?.evidence?.capability_hits) {
38
+ signals.push(`${hint.evidence.capability_hits} secured capability match${hint.evidence.capability_hits === 1 ? "" : "es"}`);
39
+ }
40
+ if (hint?.evidence?.route_hits) {
41
+ signals.push(`${hint.evidence.route_hits} route/resource match${hint.evidence.route_hits === 1 ? "" : "es"}`);
42
+ }
43
+ if (hint?.evidence?.doc_hits) {
44
+ signals.push(`${hint.evidence.doc_hits} imported doc or policy match${hint.evidence.doc_hits === 1 ? "" : "es"}`);
45
+ }
46
+ if (hint?.evidence?.provenance_hits) {
47
+ signals.push(`${hint.evidence.provenance_hits} auth middleware or policy hint${hint.evidence.provenance_hits === 1 ? "" : "s"}`);
48
+ }
49
+ if (signals.length === 0) {
50
+ return hint?.explanation || "Imported auth evidence suggests a permission rule may gate this surface.";
51
+ }
52
+ return `${hint?.explanation || "Imported auth evidence suggests a permission rule may gate this surface."} This inference is based on ${signals.join(", ")}.`;
53
+ }
54
+
55
+ /** @param {WorkflowRecord} hint @returns {any} */
56
+ export function buildAuthPermissionReviewGuidance(hint) {
57
+ return `Confirm whether permission \`${hint.permission}\` should gate the related auth-sensitive capabilities before promoting this bundle into canonical auth rules or UI visibility.`;
58
+ }
59
+
60
+ /** @param {WorkflowRecord} hint @returns {any} */
61
+ export function describeAuthClaimWhyInferred(hint) {
62
+ /** @type {any[]} */
63
+ const signals = [];
64
+ if (hint?.evidence?.capability_hits) {
65
+ signals.push(`${hint.evidence.capability_hits} secured capability match${hint.evidence.capability_hits === 1 ? "" : "es"}`);
66
+ }
67
+ if (hint?.evidence?.route_hits) {
68
+ signals.push(`${hint.evidence.route_hits} route match${hint.evidence.route_hits === 1 ? "" : "es"}`);
69
+ }
70
+ if (hint?.evidence?.participant_hits) {
71
+ signals.push(`${hint.evidence.participant_hits} participant match${hint.evidence.participant_hits === 1 ? "" : "es"}`);
72
+ }
73
+ if (hint?.evidence?.doc_hits) {
74
+ signals.push(`${hint.evidence.doc_hits} imported doc match${hint.evidence.doc_hits === 1 ? "" : "es"}`);
75
+ }
76
+ if (signals.length === 0) {
77
+ return hint?.explanation || "Imported auth-related evidence suggests this claim may matter here.";
78
+ }
79
+ return `${hint?.explanation || "Imported auth-related evidence suggests this claim may matter here."} This inference is based on ${signals.join(", ")}.`;
80
+ }
81
+
82
+ /** @param {WorkflowRecord} hint @returns {any} */
83
+ export function buildAuthClaimReviewGuidance(hint) {
84
+ const claimTarget = `claim \`${hint.claim}\` = ${formatAuthClaimValueInline(hint.claim_value)}`;
85
+ return `Confirm whether ${claimTarget} should gate the related auth-sensitive capabilities before promoting this bundle into canonical auth rules or UI visibility.`;
86
+ }
87
+
88
+ /** @param {WorkflowRecord} hint @returns {any} */
89
+ export function describeAuthOwnershipWhyInferred(hint) {
90
+ /** @type {any[]} */
91
+ const signals = [];
92
+ if (hint?.evidence?.field_hits) {
93
+ signals.push(`${hint.evidence.field_hits} ownership-style field match${hint.evidence.field_hits === 1 ? "" : "es"}`);
94
+ }
95
+ if (hint?.evidence?.capability_hits) {
96
+ signals.push(`${hint.evidence.capability_hits} secured lifecycle/detail capability match${hint.evidence.capability_hits === 1 ? "" : "es"}`);
97
+ }
98
+ if (hint?.evidence?.doc_hits) {
99
+ signals.push(`${hint.evidence.doc_hits} imported doc match${hint.evidence.doc_hits === 1 ? "" : "es"}`);
100
+ }
101
+ if (signals.length === 0) {
102
+ return hint?.explanation || "Imported field and auth evidence suggests ownership-based access control may matter here.";
103
+ }
104
+ return `${hint?.explanation || "Imported field and auth evidence suggests ownership-based access control may matter here."} This inference is based on ${signals.join(", ")}.`;
105
+ }
106
+
107
+ /** @param {WorkflowRecord} hint @returns {any} */
108
+ export function buildAuthOwnershipReviewGuidance(hint) {
109
+ return `Confirm whether field \`${hint.ownership_field}\` should drive \`${hint.ownership}\` access for the related auth-sensitive capabilities before promoting this bundle into canonical auth rules or UI visibility.`;
110
+ }
111
+
112
+ /** @param {WorkflowRecord} entry @returns {any} */
113
+ export function formatAuthRoleGuidanceInline(entry) {
114
+ return `role \`${entry.role_id}\` (${entry.confidence})`;
115
+ }
116
+
117
+ /** @param {WorkflowRecord} entry @returns {any} */
118
+ export function buildAuthRoleReviewGuidance(entry) {
119
+ if (entry.followup_action === "promote_role") {
120
+ return `Promote role \`${entry.role_id}\` first, then confirm it remains the primary participant for the related auth-sensitive capabilities before promoting linked auth changes from this bundle.`;
121
+ }
122
+ if (entry.followup_action === "link_role_to_docs") {
123
+ const docList = (entry.followup_doc_ids || []).length
124
+ ? ` docs ${(entry.followup_doc_ids || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`
125
+ : " the existing canonical docs";
126
+ return `Link role \`${entry.role_id}\` into${docList} before promoting more auth-sensitive changes from this bundle.`;
127
+ }
128
+ return `Confirm whether role \`${entry.role_id}\` should remain the primary participant for the related auth-sensitive capabilities before promoting role or auth changes from this bundle.`;
129
+ }
130
+
131
+ /** @param {WorkflowRecord} entry @returns {any} */
132
+ export function formatAuthRoleFollowupInline(entry) {
133
+ if (entry.followup_action === "promote_role") {
134
+ return "promote role";
135
+ }
136
+ if (entry.followup_action === "link_role_to_docs") {
137
+ return entry.followup_doc_ids?.length
138
+ ? `link role to docs ${(entry.followup_doc_ids || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`
139
+ : "link role to docs";
140
+ }
141
+ return "review only";
142
+ }
@@ -0,0 +1,330 @@
1
+ // @ts-check
2
+ import { confidenceRank } from "../../docs.js";
3
+ import { inferCapabilityEntityId, normalizeOpenApiPath } from "../../import-app/index.js";
4
+ import { idHintify } from "../../../text-helpers.js";
5
+ import {
6
+ buildAuthClaimReviewGuidance,
7
+ buildAuthOwnershipReviewGuidance,
8
+ buildAuthPermissionReviewGuidance,
9
+ collectAuthClaimSignalMatches,
10
+ describeAuthClaimWhyInferred,
11
+ describeAuthOwnershipWhyInferred,
12
+ describeAuthPermissionWhyInferred
13
+ } from "./formatters.js";
14
+
15
+ /** @param {WorkflowRecord} capability @returns {any} */
16
+ export function permissionResourceStemForCapability(capability) {
17
+ const endpointPath = normalizeOpenApiPath(capability?.endpoint?.path || "");
18
+ const pathSegments = endpointPath
19
+ .split("/")
20
+ .filter(Boolean)
21
+ .filter((/** @type {any} */ segment) => segment !== "{}");
22
+ const firstPathSegment = idHintify(pathSegments[0] || "");
23
+ if (firstPathSegment) {
24
+ return firstPathSegment;
25
+ }
26
+ const entityId = String(capability?.entity_id || inferCapabilityEntityId(capability) || "").replace(/^entity_/, "");
27
+ if (!entityId) {
28
+ return "resource";
29
+ }
30
+ return entityId.endsWith("s") ? entityId : `${entityId}s`;
31
+ }
32
+
33
+ /** @param {string} resource @returns {any} */
34
+ export function singularizePermissionResource(resource) {
35
+ return String(resource || "").endsWith("s") ? String(resource).slice(0, -1) : String(resource || "");
36
+ }
37
+
38
+ /** @param {WorkflowRecord} capability @param {string} resourceStem @returns {any} */
39
+ export function inferPermissionActionForCapability(capability, resourceStem) {
40
+ const capabilityId = String(capability?.id_hint || "");
41
+ const capabilityMatch = capabilityId.match(/^cap_([^_]+)_(.+)$/);
42
+ const resourceSingular = singularizePermissionResource(resourceStem);
43
+ const resourcePrefixes = [resourceStem, resourceSingular].filter(Boolean);
44
+ if (!capabilityMatch) {
45
+ const method = String(capability?.endpoint?.method || "").toUpperCase();
46
+ if (method === "GET") return "read";
47
+ if (method === "POST") return "create";
48
+ if (method === "PATCH" || method === "PUT") return "update";
49
+ if (method === "DELETE") return "delete";
50
+ return null;
51
+ }
52
+ const [, verb, remainder] = capabilityMatch;
53
+ if (verb === "get" || verb === "list") {
54
+ return "read";
55
+ }
56
+ let suffix = remainder;
57
+ for (const prefix of resourcePrefixes) {
58
+ if (suffix === prefix) {
59
+ suffix = "";
60
+ break;
61
+ }
62
+ if (suffix.startsWith(`${prefix}_`)) {
63
+ suffix = suffix.slice(prefix.length + 1);
64
+ break;
65
+ }
66
+ }
67
+ if (!suffix) {
68
+ return verb;
69
+ }
70
+ if (verb === "request") {
71
+ return `request_${suffix}`;
72
+ }
73
+ return ["create", "update", "delete"].includes(verb) ? verb : `${verb}${suffix ? `_${suffix}` : ""}`;
74
+ }
75
+
76
+ /** @param {CandidateBundle} bundle @returns {any} */
77
+ export function inferBundleAuthPermissionHints(bundle) {
78
+ const securedCapabilities = (bundle.capabilities || []).filter((/** @type {any} */ entry) => entry.auth_hint === "secured");
79
+ if (securedCapabilities.length === 0) {
80
+ return [];
81
+ }
82
+
83
+ const docEntries = bundle.docs || [];
84
+ const grouped = new Map();
85
+ for (const capability of securedCapabilities) {
86
+ const resourceStem = permissionResourceStemForCapability(capability);
87
+ const action = inferPermissionActionForCapability(capability, resourceStem);
88
+ if (!resourceStem || !action) {
89
+ continue;
90
+ }
91
+ const permission = `${resourceStem}.${action}`;
92
+ const docPatterns = [
93
+ new RegExp(`\\b${permission.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i"),
94
+ new RegExp(`\\b${resourceStem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i"),
95
+ new RegExp(`\\b${action.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i")
96
+ ];
97
+ const docMatches = collectAuthClaimSignalMatches(
98
+ docEntries,
99
+ docPatterns,
100
+ (/** @type {any} */ entry) => [entry.id, entry.title, ...(entry.provenance || []), entry.body || ""].filter(Boolean).join(" ")
101
+ );
102
+ const provenanceText = [capability.id_hint, capability.label, capability.endpoint?.path, ...(capability.provenance || [])]
103
+ .filter(Boolean)
104
+ .join(" ");
105
+ const provenanceHits = /\b(permission|policy|scope|authorize|authoriz|allow|guard|access)\b/i.test(provenanceText) ? 1 : 0;
106
+ const existing = grouped.get(permission) || {
107
+ permission,
108
+ confidence: "low",
109
+ review_required: true,
110
+ related_capabilities: [],
111
+ evidence: {
112
+ capability_hits: 0,
113
+ route_hits: 0,
114
+ doc_hits: 0,
115
+ provenance_hits: 0
116
+ },
117
+ explanation: "Secured capability naming and imported route evidence suggest this permission may gate the recovered surface."
118
+ };
119
+ existing.related_capabilities.push(capability.id_hint);
120
+ existing.evidence.capability_hits += 1;
121
+ existing.evidence.route_hits += capability.endpoint?.path ? 1 : 0;
122
+ existing.evidence.doc_hits += docMatches.length;
123
+ existing.evidence.provenance_hits += provenanceHits;
124
+ const confidence = provenanceHits > 0 || docMatches.length > 0 || capability.endpoint?.path ? "medium" : "low";
125
+ if (confidenceRank(confidence) > confidenceRank(existing.confidence)) {
126
+ existing.confidence = confidence;
127
+ }
128
+ grouped.set(permission, existing);
129
+ }
130
+
131
+ return [...grouped.values()]
132
+ .map((/** @type {any} */ entry) => ({
133
+ ...entry,
134
+ related_capabilities: [...new Set(entry.related_capabilities)].sort(),
135
+ why_inferred: describeAuthPermissionWhyInferred(entry),
136
+ review_guidance: buildAuthPermissionReviewGuidance(entry)
137
+ }))
138
+ .sort((/** @type {any} */ a, /** @type {any} */ b) => confidenceRank(b.confidence) - confidenceRank(a.confidence) || a.permission.localeCompare(b.permission));
139
+ }
140
+
141
+ /** @param {CandidateBundle} bundle @returns {any} */
142
+ export function inferBundleAuthClaimHints(bundle) {
143
+ const securedCapabilities = (bundle.capabilities || []).filter((/** @type {any} */ entry) => entry.auth_hint === "secured");
144
+ if (securedCapabilities.length === 0) {
145
+ return [];
146
+ }
147
+
148
+ const candidates = [
149
+ {
150
+ claim: "reviewer",
151
+ claim_value: "true",
152
+ confidenceFloor: "medium",
153
+ capabilityPatterns: [/\breviewer\b/i, /\breview\b/i, /\bapprove\b/i, /\breject\b/i, /\brevision\b/i],
154
+ routePatterns: [/\breviewer\b/i, /\breview\b/i, /\bapprove\b/i, /\breject\b/i, /\brevision\b/i],
155
+ participantPatterns: [/\breviewer\b/i],
156
+ docPatterns: [/\breviewer\b/i, /\breview\b/i, /\bapprove\b/i, /\breject\b/i, /\brevision\b/i],
157
+ explanation: "Review-oriented capability, route, or participant evidence suggests a reviewer claim may gate these actions."
158
+ },
159
+ {
160
+ claim: "tenant",
161
+ claim_value: null,
162
+ confidenceFloor: "low",
163
+ capabilityPatterns: [/\btenant\b/i, /\bworkspace\b/i, /\borganization\b/i, /\borg\b/i],
164
+ routePatterns: [/\btenant\b/i, /\bworkspace\b/i, /\borganization\b/i, /\borg\b/i],
165
+ participantPatterns: [],
166
+ docPatterns: [/\btenant\b/i, /\bworkspace\b/i, /\borganization\b/i, /\borg\b/i],
167
+ explanation: "Tenant or workspace naming suggests a request-scoped claim may be part of access control here."
168
+ }
169
+ ];
170
+
171
+ const routeEntries = [...(bundle.uiRoutes || []), ...securedCapabilities];
172
+ const participantEntries = [...(bundle.actors || []), ...(bundle.roles || [])];
173
+ const docEntries = bundle.docs || [];
174
+
175
+ return candidates
176
+ .map((/** @type {any} */ candidate) => {
177
+ const capabilityMatches = collectAuthClaimSignalMatches(
178
+ securedCapabilities,
179
+ candidate.capabilityPatterns,
180
+ (/** @type {any} */ entry) => [entry.id_hint, entry.label, entry.endpoint?.path, ...(entry.provenance || [])].filter(Boolean).join(" ")
181
+ );
182
+ const routeMatches = collectAuthClaimSignalMatches(
183
+ routeEntries,
184
+ candidate.routePatterns,
185
+ (/** @type {any} */ entry) => [entry.path, entry.route_path, entry.id_hint, entry.label, ...(entry.provenance || [])].filter(Boolean).join(" ")
186
+ );
187
+ const participantMatches = collectAuthClaimSignalMatches(
188
+ participantEntries,
189
+ candidate.participantPatterns,
190
+ (/** @type {any} */ entry) => [entry.id_hint, entry.label, ...(entry.provenance || [])].filter(Boolean).join(" ")
191
+ );
192
+ const docMatches = collectAuthClaimSignalMatches(
193
+ docEntries,
194
+ candidate.docPatterns,
195
+ (/** @type {any} */ entry) => [entry.id, entry.title, ...(entry.provenance || []), entry.body || ""].filter(Boolean).join(" ")
196
+ );
197
+ const signalCount = [
198
+ capabilityMatches.length > 0,
199
+ routeMatches.length > 0,
200
+ participantMatches.length > 0,
201
+ docMatches.length > 0
202
+ ].filter(Boolean).length;
203
+
204
+ if (signalCount === 0) {
205
+ return null;
206
+ }
207
+ if (candidate.claim === "reviewer" && signalCount < 2) {
208
+ return null;
209
+ }
210
+
211
+ const confidence =
212
+ participantMatches.length > 0 || (capabilityMatches.length > 0 && routeMatches.length > 0)
213
+ ? candidate.confidenceFloor
214
+ : "low";
215
+
216
+ return {
217
+ claim: candidate.claim,
218
+ claim_value: candidate.claim_value,
219
+ confidence,
220
+ review_required: true,
221
+ related_capabilities: [...new Set(capabilityMatches.map((/** @type {any} */ entry) => entry.id_hint))].sort(),
222
+ evidence: {
223
+ capability_hits: capabilityMatches.length,
224
+ route_hits: routeMatches.length,
225
+ participant_hits: participantMatches.length,
226
+ doc_hits: docMatches.length
227
+ },
228
+ explanation: candidate.explanation,
229
+ why_inferred: describeAuthClaimWhyInferred({
230
+ claim: candidate.claim,
231
+ claim_value: candidate.claim_value,
232
+ explanation: candidate.explanation,
233
+ evidence: {
234
+ capability_hits: capabilityMatches.length,
235
+ route_hits: routeMatches.length,
236
+ participant_hits: participantMatches.length,
237
+ doc_hits: docMatches.length
238
+ }
239
+ }),
240
+ review_guidance: buildAuthClaimReviewGuidance({
241
+ claim: candidate.claim,
242
+ claim_value: candidate.claim_value
243
+ })
244
+ };
245
+ })
246
+ .filter(Boolean)
247
+ .sort((/** @type {any} */ a, /** @type {any} */ b) => confidenceRank(b.confidence) - confidenceRank(a.confidence) || a.claim.localeCompare(b.claim));
248
+ }
249
+
250
+ /** @param {CandidateBundle} bundle @returns {any} */
251
+ export function inferBundleAuthOwnershipHints(bundle) {
252
+ const securedCapabilities = (bundle.capabilities || []).filter((/** @type {any} */ entry) => entry.auth_hint === "secured");
253
+ if (securedCapabilities.length === 0) {
254
+ return [];
255
+ }
256
+
257
+ const entityFieldEntries = ((bundle.importedFieldEvidence || []).length > 0 ? bundle.importedFieldEvidence : (bundle.entities || []).flatMap((/** @type {any} */ entity) => (entity.fields || []).map((/** @type {any} */ field) => ({
258
+ entity_id: entity.id_hint,
259
+ name: field.name,
260
+ field_type: field.field_type,
261
+ required: field.required
262
+ }))));
263
+ const docEntries = bundle.docs || [];
264
+ const ownershipScopedCapabilities = securedCapabilities.filter((/** @type {any} */ entry) =>
265
+ /^cap_(get|update|close|complete|archive|delete|submit|request|approve|reject)_/.test(entry.id_hint || "")
266
+ );
267
+ if (entityFieldEntries.length === 0 || ownershipScopedCapabilities.length === 0) {
268
+ return [];
269
+ }
270
+
271
+ const candidates = [
272
+ {
273
+ ownership: "owner_or_admin",
274
+ ownership_field: "owner_id",
275
+ confidenceFloor: "medium",
276
+ fieldPatterns: [/^owner_id$/i, /^author_id$/i],
277
+ docPatterns: [/\bowner\b/i, /\bauthor\b/i],
278
+ explanation: "Ownership-style field naming suggests this bundle may authorize detail or lifecycle actions based on resource ownership."
279
+ },
280
+ {
281
+ ownership: "owner_or_admin",
282
+ ownership_field: "assignee_id",
283
+ confidenceFloor: "medium",
284
+ fieldPatterns: [/^assignee_id$/i],
285
+ docPatterns: [/\bassignee\b/i, /\bassigned\b/i],
286
+ explanation: "Assignment-style field naming suggests this bundle may authorize detail or lifecycle actions based on the assigned user."
287
+ }
288
+ ];
289
+
290
+ return candidates
291
+ .map((/** @type {any} */ candidate) => {
292
+ const fieldMatches = entityFieldEntries.filter((/** @type {any} */ entry) => candidate.fieldPatterns.some((/** @type {any} */ pattern) => pattern.test(entry.name || "")));
293
+ const docMatches = collectAuthClaimSignalMatches(
294
+ docEntries,
295
+ candidate.docPatterns,
296
+ (/** @type {any} */ entry) => [entry.id, entry.title, ...(entry.provenance || []), entry.body || ""].filter(Boolean).join(" ")
297
+ );
298
+ if (fieldMatches.length === 0) {
299
+ return null;
300
+ }
301
+ const relatedCapabilities = ownershipScopedCapabilities.map((/** @type {any} */ entry) => entry.id_hint).sort();
302
+ const evidence = {
303
+ field_hits: fieldMatches.length,
304
+ capability_hits: relatedCapabilities.length,
305
+ doc_hits: docMatches.length
306
+ };
307
+ return {
308
+ ownership: candidate.ownership,
309
+ ownership_field: candidate.ownership_field,
310
+ confidence: candidate.confidenceFloor,
311
+ review_required: true,
312
+ related_capabilities: relatedCapabilities,
313
+ related_entities: [...new Set(fieldMatches.map((/** @type {any} */ entry) => entry.entity_id))].sort(),
314
+ evidence,
315
+ explanation: candidate.explanation,
316
+ why_inferred: describeAuthOwnershipWhyInferred({
317
+ ownership: candidate.ownership,
318
+ ownership_field: candidate.ownership_field,
319
+ explanation: candidate.explanation,
320
+ evidence
321
+ }),
322
+ review_guidance: buildAuthOwnershipReviewGuidance({
323
+ ownership: candidate.ownership,
324
+ ownership_field: candidate.ownership_field
325
+ })
326
+ };
327
+ })
328
+ .filter(Boolean)
329
+ .sort((/** @type {any} */ a, /** @type {any} */ b) => confidenceRank(b.confidence) - confidenceRank(a.confidence) || a.ownership_field.localeCompare(b.ownership_field));
330
+ }